import { IconButton } from "./button"; import { ErrorBoundary } from "./error"; import styles from "./mcp-market.module.scss"; import EditIcon from "../icons/edit.svg"; import AddIcon from "../icons/add.svg"; import CloseIcon from "../icons/close.svg"; import DeleteIcon from "../icons/delete.svg"; import RestartIcon from "../icons/reload.svg"; import EyeIcon from "../icons/eye.svg"; import GithubIcon from "../icons/github.svg"; import { List, ListItem, Modal, showToast } from "./ui-lib"; import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; import { addMcpServer, getClientsStatus, getClientTools, getMcpConfigFromFile, isMcpEnabled, pauseMcpServer, restartAllClients, 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"; import { Path } from "../constant"; interface ConfigProperty { type: string; description?: string; required?: boolean; minItems?: number; } export function McpMarketPage() { const navigate = useNavigate(); const [mcpEnabled, setMcpEnabled] = useState(false); const [searchText, setSearchText] = useState(""); const [userConfig, setUserConfig] = useState>({}); const [editingServerId, setEditingServerId] = useState(); const [tools, setTools] = useState(null); const [viewingServerId, setViewingServerId] = useState(); const [isLoading, setIsLoading] = useState(false); const [config, setConfig] = useState(); const [clientStatuses, setClientStatuses] = useState< Record >({}); const [loadingPresets, setLoadingPresets] = useState(true); const [presetServers, setPresetServers] = useState([]); const [loadingStates, setLoadingStates] = useState>( {}, ); // 检查 MCP 是否启用 useEffect(() => { const checkMcpStatus = async () => { const enabled = await isMcpEnabled(); setMcpEnabled(enabled); if (!enabled) { navigate(Path.Home); } }; checkMcpStatus(); }, [navigate]); // 添加状态轮询 useEffect(() => { if (!mcpEnabled || !config) return; const updateStatuses = async () => { const statuses = await getClientsStatus(); setClientStatuses(statuses); }; // 立即执行一次 updateStatuses(); // 每 1000ms 轮询一次 const timer = setInterval(updateStatuses, 1000); return () => clearInterval(timer); }, [mcpEnabled, config]); // 加载预设服务器 useEffect(() => { const loadPresetServers = async () => { if (!mcpEnabled) return; try { setLoadingPresets(true); const response = await fetch("https://nextchat.club/mcp/list"); if (!response.ok) { throw new Error("Failed to load preset servers"); } const data = await response.json(); setPresetServers(data?.data ?? []); } catch (error) { console.error("Failed to load preset servers:", error); showToast("Failed to load preset servers"); } finally { setLoadingPresets(false); } }; loadPresetServers(); }, [mcpEnabled]); // 加载初始状态 useEffect(() => { const loadInitialState = async () => { if (!mcpEnabled) return; try { setIsLoading(true); const config = await getMcpConfigFromFile(); setConfig(config); // 获取所有客户端的状态 const statuses = await getClientsStatus(); setClientStatuses(statuses); } catch (error) { console.error("Failed to load initial state:", error); showToast("Failed to load initial state"); } finally { setIsLoading(false); } }; loadInitialState(); }, [mcpEnabled]); // 加载当前编辑服务器的配置 useEffect(() => { if (!editingServerId || !config) return; const currentConfig = config.mcpServers[editingServerId]; if (currentConfig) { // 从当前配置中提取用户配置 const preset = presetServers.find((s) => s.id === editingServerId); if (preset?.configSchema) { const userConfig: Record = {}; Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => { if (mapping.type === "spread") { // For spread types, extract the array from args. const startPos = mapping.position ?? 0; userConfig[key] = currentConfig.args.slice(startPos); } else if (mapping.type === "single") { // For single types, get a single value userConfig[key] = currentConfig.args[mapping.position ?? 0]; } else if ( mapping.type === "env" && mapping.key && currentConfig.env ) { // For env types, get values from environment variables userConfig[key] = currentConfig.env[mapping.key]; } }); setUserConfig(userConfig); } } else { setUserConfig({}); } }, [editingServerId, config, presetServers]); if (!mcpEnabled) { return null; } // 检查服务器是否已添加 const isServerAdded = (id: string) => { return id in (config?.mcpServers ?? {}); }; // 保存服务器配置 const saveServerConfig = async () => { const preset = presetServers.find((s) => s.id === editingServerId); if (!preset || !preset.configSchema || !editingServerId) return; const savingServerId = editingServerId; setEditingServerId(undefined); try { updateLoadingState(savingServerId, "Updating configuration..."); // 构建服务器配置 const args = [...preset.baseArgs]; const env: Record = {}; Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => { const value = userConfig[key]; if (mapping.type === "spread" && Array.isArray(value)) { const pos = mapping.position ?? 0; args.splice(pos, 0, ...value); } else if ( mapping.type === "single" && mapping.position !== undefined ) { args[mapping.position] = value; } else if ( mapping.type === "env" && mapping.key && typeof value === "string" ) { env[mapping.key] = value; } }); const serverConfig: ServerConfig = { command: preset.command, args, ...(Object.keys(env).length > 0 ? { env } : {}), }; const newConfig = await addMcpServer(savingServerId, serverConfig); setConfig(newConfig); showToast("Server configuration updated successfully"); } catch (error) { showToast( error instanceof Error ? error.message : "Failed to save configuration", ); } finally { updateLoadingState(savingServerId, null); } }; // 获取服务器支持的 Tools const loadTools = async (id: string) => { try { const result = await getClientTools(id); if (result) { setTools(result); } else { throw new Error("Failed to load tools"); } } catch (error) { showToast("Failed to load tools"); console.error(error); setTools(null); } }; // 更新加载状态的辅助函数 const updateLoadingState = (id: string, message: string | null) => { setLoadingStates((prev) => { if (message === null) { const { [id]: _, ...rest } = prev; return rest; } return { ...prev, [id]: message }; }); }; // 修改添加服务器函数 const addServer = async (preset: PresetServer) => { if (!preset.configurable) { try { const serverId = preset.id; updateLoadingState(serverId, "Creating MCP client..."); const serverConfig: ServerConfig = { command: preset.command, args: [...preset.baseArgs], }; const newConfig = await addMcpServer(preset.id, serverConfig); setConfig(newConfig); // 更新状态 const statuses = await getClientsStatus(); setClientStatuses(statuses); } finally { updateLoadingState(preset.id, null); } } else { // 如果需要配置,打开配置对话框 setEditingServerId(preset.id); setUserConfig({}); } }; // 修改暂停服务器函数 const pauseServer = async (id: string) => { try { updateLoadingState(id, "Stopping server..."); const newConfig = await pauseMcpServer(id); setConfig(newConfig); showToast("Server stopped successfully"); } catch (error) { showToast("Failed to stop server"); console.error(error); } finally { updateLoadingState(id, null); } }; // Restart server const restartServer = async (id: string) => { try { updateLoadingState(id, "Starting server..."); await resumeMcpServer(id); } catch (error) { showToast( error instanceof Error ? error.message : "Failed to start server, please check logs", ); console.error(error); } finally { updateLoadingState(id, null); } }; // Restart all clients const handleRestartAll = async () => { try { updateLoadingState("all", "Restarting all servers..."); const newConfig = await restartAllClients(); setConfig(newConfig); showToast("Restarting all clients"); } catch (error) { showToast("Failed to restart clients"); console.error(error); } finally { updateLoadingState("all", null); } }; // Render configuration form const renderConfigForm = () => { const preset = presetServers.find((s) => s.id === editingServerId); if (!preset?.configSchema) return null; return Object.entries(preset.configSchema.properties).map( ([key, prop]: [string, ConfigProperty]) => { if (prop.type === "array") { const currentValue = userConfig[key as keyof typeof userConfig] || []; const itemLabel = (prop as any).itemLabel || key; const addButtonText = (prop as any).addButtonText || `Add ${itemLabel}`; return (
{(currentValue as string[]).map( (value: string, index: number) => (
{ const newValue = [...currentValue] as string[]; newValue[index] = e.target.value; setUserConfig({ ...userConfig, [key]: newValue }); }} /> } className={styles["delete-button"]} onClick={() => { const newValue = [...currentValue] as string[]; newValue.splice(index, 1); setUserConfig({ ...userConfig, [key]: newValue }); }} />
), )} } text={addButtonText} className={styles["add-button"]} bordered onClick={() => { const newValue = [...currentValue, ""] as string[]; setUserConfig({ ...userConfig, [key]: newValue }); }} />
); } else if (prop.type === "string") { const currentValue = userConfig[key as keyof typeof userConfig] || ""; return ( { setUserConfig({ ...userConfig, [key]: e.target.value }); }} /> ); } return null; }, ); }; const checkServerStatus = (clientId: string) => { return clientStatuses[clientId] || { status: "undefined", errorMsg: null }; }; const getServerStatusDisplay = (clientId: string) => { const status = checkServerStatus(clientId); const statusMap = { undefined: null, // 未配置/未找到不显示 // 添加初始化状态 initializing: ( Initializing ), paused: ( Stopped ), active: Running, error: ( Error : {status.errorMsg} ), }; return statusMap[status.status]; }; // Get the type of operation status const getOperationStatusType = (message: string) => { if (message.toLowerCase().includes("stopping")) return "stopping"; if (message.toLowerCase().includes("starting")) return "starting"; if (message.toLowerCase().includes("error")) return "error"; return "default"; }; // 渲染服务器列表 const renderServerList = () => { if (loadingPresets) { return (
Loading preset server list...
); } if (!Array.isArray(presetServers) || presetServers.length === 0) { return (
No servers available
); } return presetServers .filter((server) => { if (searchText.length === 0) return true; const searchLower = searchText.toLowerCase(); return ( server.name.toLowerCase().includes(searchLower) || server.description.toLowerCase().includes(searchLower) || server.tags.some((tag) => tag.toLowerCase().includes(searchLower)) ); }) .sort((a, b) => { const aStatus = checkServerStatus(a.id).status; const bStatus = checkServerStatus(b.id).status; const aLoading = loadingStates[a.id]; const bLoading = loadingStates[b.id]; // 定义状态优先级 const statusPriority: Record = { error: 0, // Highest priority for error status active: 1, // Second for active initializing: 2, // Initializing starting: 3, // Starting stopping: 4, // Stopping paused: 5, // Paused undefined: 6, // Lowest priority for undefined }; // Get actual status (including loading status) const getEffectiveStatus = (status: string, loading?: string) => { if (loading) { const operationType = getOperationStatusType(loading); return operationType === "default" ? status : operationType; } if (status === "initializing" && !loading) { return "active"; } return status; }; const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading); const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading); // 首先按状态排序 if (aEffectiveStatus !== bEffectiveStatus) { return ( (statusPriority[aEffectiveStatus] ?? 6) - (statusPriority[bEffectiveStatus] ?? 6) ); } // Sort by name when statuses are the same return a.name.localeCompare(b.name); }) .map((server) => (
{server.name} {loadingStates[server.id] && ( {loadingStates[server.id]} )} {!loadingStates[server.id] && getServerStatusDisplay(server.id)} {server.repo && ( )}
{server.tags.map((tag, index) => ( {tag} ))}
{server.description}
{isServerAdded(server.id) ? ( <> {server.configurable && ( } text="Configure" onClick={() => setEditingServerId(server.id)} disabled={isLoading} /> )} {checkServerStatus(server.id).status === "paused" ? ( <> } text="Start" onClick={() => restartServer(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" onClick={() => addServer(server)} disabled={isLoading} /> )}
)); }; return (
MCP Market {loadingStates["all"] && ( {loadingStates["all"]} )}
{Object.keys(config?.mcpServers ?? {}).length} servers configured
} bordered onClick={handleRestartAll} text="Restart All" disabled={isLoading} />
} bordered onClick={() => navigate(-1)} disabled={isLoading} />
setSearchText(e.currentTarget.value)} />
{renderServerList()}
{/*编辑服务器配置*/} {editingServerId && (
!isLoading && setEditingServerId(undefined)} actions={[ setEditingServerId(undefined)} bordered disabled={isLoading} />, , ]} > {renderConfigForm()}
)} {viewingServerId && (
setViewingServerId(undefined)} actions={[ setViewingServerId(undefined)} bordered />, ]} >
{isLoading ? (
Loading...
) : tools?.tools ? ( tools.tools.map( (tool: ListToolsResponse["tools"], index: number) => (
{tool.name}
{tool.description}
), ) ) : (
No tools available
)}
)}
); }