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 { List, ListItem, Modal, showToast } from "./ui-lib"; import { useNavigate } from "react-router-dom"; import { useState, useEffect } from "react"; import presetServersJson from "../mcp/preset-server.json"; const presetServers = presetServersJson as PresetServer[]; import { getMcpConfig, updateMcpConfig, getClientPrimitives, restartAllClients, getClientErrors, refreshClientStatus, } from "../mcp/actions"; import { McpConfig, PresetServer, ServerConfig } from "../mcp/types"; import clsx from "clsx"; interface ConfigProperty { type: string; description?: string; required?: boolean; minItems?: number; } export function McpMarketPage() { const navigate = useNavigate(); const [searchText, setSearchText] = useState(""); const [config, setConfig] = useState({ mcpServers: {} }); const [editingServerId, setEditingServerId] = useState(); const [viewingServerId, setViewingServerId] = useState(); const [primitives, setPrimitives] = useState([]); const [userConfig, setUserConfig] = useState>({}); const [isLoading, setIsLoading] = useState(false); const [clientErrors, setClientErrors] = useState< Record >({}); // 更新服务器状态 const updateServerStatus = async () => { await refreshClientStatus(); const errors = await getClientErrors(); setClientErrors(errors); }; // 初始加载配置 useEffect(() => { const init = async () => { try { setIsLoading(true); const data = await getMcpConfig(); setConfig(data); await updateServerStatus(); } catch (error) { showToast("Failed to load configuration"); console.error(error); } finally { setIsLoading(false); } }; init().then(); }, []); // 保存配置 const saveConfig = async (newConfig: McpConfig) => { try { setIsLoading(true); await updateMcpConfig(newConfig); setConfig(newConfig); // 配置改变时需要重新初始化 await restartAllClients(); await updateServerStatus(); showToast("Configuration saved successfully"); } catch (error) { showToast("Failed to save configuration"); console.error(error); } finally { setIsLoading(false); } }; // 检查服务器是否已添加 const isServerAdded = (id: string) => { return id in config.mcpServers; }; // 加载当前编辑服务器的配置 useEffect(() => { if (editingServerId) { 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.mcpServers]); // 保存服务器配置 const saveServerConfig = async () => { const preset = presetServers.find((s) => s.id === editingServerId); if (!preset || !preset.configSchema || !editingServerId) return; try { // 构建服务器配置 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 = { ...config, mcpServers: { ...config.mcpServers, [editingServerId]: serverConfig, }, }; await saveConfig(newConfig); setEditingServerId(undefined); showToast("Server configuration saved successfully"); } catch (error) { showToast( error instanceof Error ? error.message : "Failed to save configuration", ); } }; // 渲染配置表单 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] || []; 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="Add Path" 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; }, ); }; // 获取服务器的 Primitives const loadPrimitives = async (id: string) => { try { setIsLoading(true); const result = await getClientPrimitives(id); if (result) { setPrimitives(result); } else { showToast("Server is not running"); setPrimitives([]); } } catch (error) { showToast("Failed to load primitives"); console.error(error); setPrimitives([]); } finally { setIsLoading(false); } }; // 重启所有客户端 const handleRestart = async () => { try { setIsLoading(true); await restartAllClients(); await updateServerStatus(); showToast("All clients restarted successfully"); } 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 = { ...config, mcpServers: { ...config.mcpServers, [preset.id]: serverConfig, }, }; await saveConfig(newConfig); } finally { setIsLoading(false); } } else { // 如果需要配置,打开配置对话框 setEditingServerId(preset.id); setUserConfig({}); } }; // 移除服务器 const removeServer = async (id: string) => { try { setIsLoading(true); const { [id]: _, ...rest } = config.mcpServers; const newConfig = { ...config, mcpServers: rest, }; await saveConfig(newConfig); } finally { setIsLoading(false); } }; return (
MCP Market {isLoading && ( Loading... )}
{Object.keys(config.mcpServers).length} servers configured
} bordered onClick={handleRestart} text="Restart All" disabled={isLoading} />
} bordered onClick={() => navigate(-1)} disabled={isLoading} />
setSearchText(e.currentTarget.value)} />
{presetServers .filter( (m) => searchText.length === 0 || m.name.toLowerCase().includes(searchText.toLowerCase()) || m.description .toLowerCase() .includes(searchText.toLowerCase()), ) .sort((a, b) => { const aAdded = isServerAdded(a.id); const bAdded = isServerAdded(b.id); const aError = clientErrors[a.id] !== null; const bError = clientErrors[b.id] !== null; if (aAdded !== bAdded) { return aAdded ? -1 : 1; } if (aAdded && bAdded) { if (aError !== bError) { return aError ? -1 : 1; } } return 0; }) .map((server) => (
{server.name} {isServerAdded(server.id) && ( {clientErrors[server.id] === null ? "Active" : "Error"} {clientErrors[server.id] && ( : {clientErrors[server.id]} )} )}
{server.description}
{isServerAdded(server.id) ? ( <> {server.configurable && ( } text="Configure" className={clsx({ [styles["action-error"]]: clientErrors[server.id] !== null, })} onClick={() => setEditingServerId(server.id)} disabled={isLoading} /> )} {isServerAdded(server.id) && ( } text="Tools" onClick={async () => { if (clientErrors[server.id] !== null) { showToast("Server is not running"); return; } setViewingServerId(server.id); await loadPrimitives(server.id); }} disabled={isLoading} /> )} } text="Remove" className={styles["action-danger"]} onClick={() => removeServer(server.id)} disabled={isLoading} /> ) : ( } text="Add" className={styles["action-primary"]} onClick={() => addServer(server)} disabled={isLoading} /> )}
))}
{editingServerId && (
!isLoading && setEditingServerId(undefined)} actions={[ setEditingServerId(undefined)} bordered disabled={isLoading} />, , ]} > {renderConfigForm()}
)} {viewingServerId && (
setViewingServerId(undefined)} actions={[ setViewingServerId(undefined)} bordered />, ]} >
{isLoading ? (
Loading...
) : primitives.filter((p) => p.type === "tool").length > 0 ? ( primitives .filter((p) => p.type === "tool") .map((primitive, index) => (
{primitive.value.name}
{primitive.value.description && (
{primitive.value.description}
)}
)) ) : (
No tools available
)}
)}
); }