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 presetServersJson from "../mcp/preset-server.json"; import { addMcpServer, getClientStatus, getClientTools, getMcpConfigFromFile, 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[]; interface ConfigProperty { type: string; description?: string; required?: boolean; minItems?: number; } export function McpMarketPage() { const navigate = useNavigate(); 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 isServerAdded = (id: string) => { return id in (config?.mcpServers ?? {}); }; // 从服务器获取初始状态 useEffect(() => { const loadInitialState = async () => { try { setIsLoading(true); const config = await getMcpConfigFromFile(); setConfig(config); // 获取所有客户端的状态 const statuses: Record = {}; for (const clientId of Object.keys(config.mcpServers)) { statuses[clientId] = await getClientStatus(clientId); } setClientStatuses(statuses); } catch (error) { console.error("Failed to load initial state:", error); showToast("Failed to load initial state"); } finally { setIsLoading(false); } }; loadInitialState(); }, []); // Debug: 监控状态变化 useEffect(() => { console.log("MCP Market - Current config:", config); console.log("MCP Market - Current clientStatuses:", clientStatuses); }, [config, clientStatuses]); // 加载当前编辑服务器的配置 useEffect(() => { if (editingServerId && config) { 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") { // 对于 spread 类型,从 args 中提取数组 const startPos = mapping.position ?? 0; userConfig[key] = currentConfig.args.slice(startPos); } else if (mapping.type === "single") { // 对于 single 类型,获取单个值 userConfig[key] = currentConfig.args[mapping.position ?? 0]; } else if ( mapping.type === "env" && mapping.key && currentConfig.env ) { // 对于 env 类型,从环境变量中获取值 userConfig[key] = currentConfig.env[mapping.key]; } }); setUserConfig(userConfig); } } else { setUserConfig({}); } } }, [editingServerId, config]); // 保存服务器配置 const saveServerConfig = async () => { const preset = presetServers.find((s) => s.id === editingServerId); if (!preset || !preset.configSchema || !editingServerId) return; try { setIsLoading(true); // 构建服务器配置 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(editingServerId, serverConfig); setConfig(newConfig); // 更新状态 const status = await getClientStatus(editingServerId); setClientStatuses((prev) => ({ ...prev, [editingServerId]: status, })); setEditingServerId(undefined); showToast("Server configuration saved successfully"); } catch (error) { showToast( error instanceof Error ? error.message : "Failed to save configuration", ); } finally { setIsLoading(false); } }; // 获取服务器支持的 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 handleRestartAll = async () => { try { setIsLoading(true); const newConfig = await restartAllClients(); setConfig(newConfig); // 更新所有客户端状态 const statuses: Record = {}; for (const clientId of Object.keys(newConfig.mcpServers)) { statuses[clientId] = await getClientStatus(clientId); } setClientStatuses(statuses); showToast("Successfully restarted all clients"); } catch (error) { showToast("Failed to restart clients"); console.error(error); } finally { setIsLoading(false); } }; // 添加服务器 const addServer = async (preset: PresetServer) => { if (!preset.configurable) { try { setIsLoading(true); showToast("Creating MCP client..."); // 如果服务器不需要配置,直接添加 const serverConfig: ServerConfig = { command: preset.command, args: [...preset.baseArgs], }; const newConfig = await addMcpServer(preset.id, serverConfig); setConfig(newConfig); // 更新状态 const status = await getClientStatus(preset.id); setClientStatuses((prev) => ({ ...prev, [preset.id]: status, })); } finally { setIsLoading(false); } } else { // 如果需要配置,打开配置对话框 setEditingServerId(preset.id); setUserConfig({}); } }; // 移除服务器 // 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); showToast("Stopping server..."); const newConfig = await pauseMcpServer(id); setConfig(newConfig); // 更新状态为暂停 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); } }; // 渲染配置表单 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, // 未配置/未找到不显示 paused: ( Stopped ), active: Running, error: ( Error : {status.errorMsg} ), }; return statusMap[status.status]; }; // 渲染服务器列表 const renderServerList = () => { 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 statusPriority: Record = { error: 0, // 最高优先级 active: 1, // 运行中 paused: 2, // 已暂停 undefined: 3, // 未配置/未找到 }; // 首先按状态排序 if (aStatus !== bStatus) { return ( (statusPriority[aStatus] || 3) - (statusPriority[bStatus] || 3) ); } // 然后按名称排序 return a.name.localeCompare(b.name); }) .map((server) => (
{server.name} {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={() => 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" onClick={() => addServer(server)} disabled={isLoading} /> )}
)); }; return (
MCP Market {isLoading && ( Loading... )}
{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()}
)} {/*支持的Tools*/} {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
)}
)}
); }