mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 16:23:41 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			756 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			756 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<Record<string, any>>({});
 | 
						|
  const [editingServerId, setEditingServerId] = useState<string | undefined>();
 | 
						|
  const [tools, setTools] = useState<ListToolsResponse["tools"] | null>(null);
 | 
						|
  const [viewingServerId, setViewingServerId] = useState<string | undefined>();
 | 
						|
  const [isLoading, setIsLoading] = useState(false);
 | 
						|
  const [config, setConfig] = useState<McpConfigData>();
 | 
						|
  const [clientStatuses, setClientStatuses] = useState<
 | 
						|
    Record<string, ServerStatusResponse>
 | 
						|
  >({});
 | 
						|
  const [loadingPresets, setLoadingPresets] = useState(true);
 | 
						|
  const [presetServers, setPresetServers] = useState<PresetServer[]>([]);
 | 
						|
  const [loadingStates, setLoadingStates] = useState<Record<string, string>>(
 | 
						|
    {},
 | 
						|
  );
 | 
						|
 | 
						|
  // 检查 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<string, any> = {};
 | 
						|
        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<string, string> = {};
 | 
						|
 | 
						|
      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 (
 | 
						|
            <ListItem
 | 
						|
              key={key}
 | 
						|
              title={key}
 | 
						|
              subTitle={prop.description}
 | 
						|
              vertical
 | 
						|
            >
 | 
						|
              <div className={styles["path-list"]}>
 | 
						|
                {(currentValue as string[]).map(
 | 
						|
                  (value: string, index: number) => (
 | 
						|
                    <div key={index} className={styles["path-item"]}>
 | 
						|
                      <input
 | 
						|
                        type="text"
 | 
						|
                        value={value}
 | 
						|
                        placeholder={`${itemLabel} ${index + 1}`}
 | 
						|
                        onChange={(e) => {
 | 
						|
                          const newValue = [...currentValue] as string[];
 | 
						|
                          newValue[index] = e.target.value;
 | 
						|
                          setUserConfig({ ...userConfig, [key]: newValue });
 | 
						|
                        }}
 | 
						|
                      />
 | 
						|
                      <IconButton
 | 
						|
                        icon={<DeleteIcon />}
 | 
						|
                        className={styles["delete-button"]}
 | 
						|
                        onClick={() => {
 | 
						|
                          const newValue = [...currentValue] as string[];
 | 
						|
                          newValue.splice(index, 1);
 | 
						|
                          setUserConfig({ ...userConfig, [key]: newValue });
 | 
						|
                        }}
 | 
						|
                      />
 | 
						|
                    </div>
 | 
						|
                  ),
 | 
						|
                )}
 | 
						|
                <IconButton
 | 
						|
                  icon={<AddIcon />}
 | 
						|
                  text={addButtonText}
 | 
						|
                  className={styles["add-button"]}
 | 
						|
                  bordered
 | 
						|
                  onClick={() => {
 | 
						|
                    const newValue = [...currentValue, ""] as string[];
 | 
						|
                    setUserConfig({ ...userConfig, [key]: newValue });
 | 
						|
                  }}
 | 
						|
                />
 | 
						|
              </div>
 | 
						|
            </ListItem>
 | 
						|
          );
 | 
						|
        } else if (prop.type === "string") {
 | 
						|
          const currentValue = userConfig[key as keyof typeof userConfig] || "";
 | 
						|
          return (
 | 
						|
            <ListItem key={key} title={key} subTitle={prop.description}>
 | 
						|
              <input
 | 
						|
                aria-label={key}
 | 
						|
                type="text"
 | 
						|
                value={currentValue}
 | 
						|
                placeholder={`Enter ${key}`}
 | 
						|
                onChange={(e) => {
 | 
						|
                  setUserConfig({ ...userConfig, [key]: e.target.value });
 | 
						|
                }}
 | 
						|
              />
 | 
						|
            </ListItem>
 | 
						|
          );
 | 
						|
        }
 | 
						|
        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: (
 | 
						|
        <span className={clsx(styles["server-status"], styles["initializing"])}>
 | 
						|
          Initializing
 | 
						|
        </span>
 | 
						|
      ),
 | 
						|
      paused: (
 | 
						|
        <span className={clsx(styles["server-status"], styles["stopped"])}>
 | 
						|
          Stopped
 | 
						|
        </span>
 | 
						|
      ),
 | 
						|
      active: <span className={styles["server-status"]}>Running</span>,
 | 
						|
      error: (
 | 
						|
        <span className={clsx(styles["server-status"], styles["error"])}>
 | 
						|
          Error
 | 
						|
          <span className={styles["error-message"]}>: {status.errorMsg}</span>
 | 
						|
        </span>
 | 
						|
      ),
 | 
						|
    };
 | 
						|
 | 
						|
    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 (
 | 
						|
        <div className={styles["loading-container"]}>
 | 
						|
          <div className={styles["loading-text"]}>
 | 
						|
            Loading preset server list...
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (!Array.isArray(presetServers) || presetServers.length === 0) {
 | 
						|
      return (
 | 
						|
        <div className={styles["empty-container"]}>
 | 
						|
          <div className={styles["empty-text"]}>No servers available</div>
 | 
						|
        </div>
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    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<string, number> = {
 | 
						|
          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) => (
 | 
						|
        <div
 | 
						|
          className={clsx(styles["mcp-market-item"], {
 | 
						|
            [styles["loading"]]: loadingStates[server.id],
 | 
						|
          })}
 | 
						|
          key={server.id}
 | 
						|
        >
 | 
						|
          <div className={styles["mcp-market-header"]}>
 | 
						|
            <div className={styles["mcp-market-title"]}>
 | 
						|
              <div className={styles["mcp-market-name"]}>
 | 
						|
                {server.name}
 | 
						|
                {loadingStates[server.id] && (
 | 
						|
                  <span
 | 
						|
                    className={styles["operation-status"]}
 | 
						|
                    data-status={getOperationStatusType(
 | 
						|
                      loadingStates[server.id],
 | 
						|
                    )}
 | 
						|
                  >
 | 
						|
                    {loadingStates[server.id]}
 | 
						|
                  </span>
 | 
						|
                )}
 | 
						|
                {!loadingStates[server.id] && getServerStatusDisplay(server.id)}
 | 
						|
                {server.repo && (
 | 
						|
                  <a
 | 
						|
                    href={server.repo}
 | 
						|
                    target="_blank"
 | 
						|
                    rel="noopener noreferrer"
 | 
						|
                    className={styles["repo-link"]}
 | 
						|
                    title="Open repository"
 | 
						|
                  >
 | 
						|
                    <GithubIcon />
 | 
						|
                  </a>
 | 
						|
                )}
 | 
						|
              </div>
 | 
						|
              <div className={styles["tags-container"]}>
 | 
						|
                {server.tags.map((tag, index) => (
 | 
						|
                  <span key={index} className={styles["tag"]}>
 | 
						|
                    {tag}
 | 
						|
                  </span>
 | 
						|
                ))}
 | 
						|
              </div>
 | 
						|
              <div
 | 
						|
                className={clsx(styles["mcp-market-info"], "one-line")}
 | 
						|
                title={server.description}
 | 
						|
              >
 | 
						|
                {server.description}
 | 
						|
              </div>
 | 
						|
            </div>
 | 
						|
            <div className={styles["mcp-market-actions"]}>
 | 
						|
              {isServerAdded(server.id) ? (
 | 
						|
                <>
 | 
						|
                  {server.configurable && (
 | 
						|
                    <IconButton
 | 
						|
                      icon={<EditIcon />}
 | 
						|
                      text="Configure"
 | 
						|
                      onClick={() => setEditingServerId(server.id)}
 | 
						|
                      disabled={isLoading}
 | 
						|
                    />
 | 
						|
                  )}
 | 
						|
                  {checkServerStatus(server.id).status === "paused" ? (
 | 
						|
                    <>
 | 
						|
                      <IconButton
 | 
						|
                        icon={<PlayIcon />}
 | 
						|
                        text="Start"
 | 
						|
                        onClick={() => restartServer(server.id)}
 | 
						|
                        disabled={isLoading}
 | 
						|
                      />
 | 
						|
                      {/* <IconButton
 | 
						|
                        icon={<DeleteIcon />}
 | 
						|
                        text="Remove"
 | 
						|
                        onClick={() => removeServer(server.id)}
 | 
						|
                        disabled={isLoading}
 | 
						|
                      /> */}
 | 
						|
                    </>
 | 
						|
                  ) : (
 | 
						|
                    <>
 | 
						|
                      <IconButton
 | 
						|
                        icon={<EyeIcon />}
 | 
						|
                        text="Tools"
 | 
						|
                        onClick={async () => {
 | 
						|
                          setViewingServerId(server.id);
 | 
						|
                          await loadTools(server.id);
 | 
						|
                        }}
 | 
						|
                        disabled={
 | 
						|
                          isLoading ||
 | 
						|
                          checkServerStatus(server.id).status === "error"
 | 
						|
                        }
 | 
						|
                      />
 | 
						|
                      <IconButton
 | 
						|
                        icon={<StopIcon />}
 | 
						|
                        text="Stop"
 | 
						|
                        onClick={() => pauseServer(server.id)}
 | 
						|
                        disabled={isLoading}
 | 
						|
                      />
 | 
						|
                    </>
 | 
						|
                  )}
 | 
						|
                </>
 | 
						|
              ) : (
 | 
						|
                <IconButton
 | 
						|
                  icon={<AddIcon />}
 | 
						|
                  text="Add"
 | 
						|
                  onClick={() => addServer(server)}
 | 
						|
                  disabled={isLoading}
 | 
						|
                />
 | 
						|
              )}
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
      ));
 | 
						|
  };
 | 
						|
 | 
						|
  return (
 | 
						|
    <ErrorBoundary>
 | 
						|
      <div className={styles["mcp-market-page"]}>
 | 
						|
        <div className="window-header">
 | 
						|
          <div className="window-header-title">
 | 
						|
            <div className="window-header-main-title">
 | 
						|
              MCP Market
 | 
						|
              {loadingStates["all"] && (
 | 
						|
                <span className={styles["loading-indicator"]}>
 | 
						|
                  {loadingStates["all"]}
 | 
						|
                </span>
 | 
						|
              )}
 | 
						|
            </div>
 | 
						|
            <div className="window-header-sub-title">
 | 
						|
              {Object.keys(config?.mcpServers ?? {}).length} servers configured
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
 | 
						|
          <div className="window-actions">
 | 
						|
            <div className="window-action-button">
 | 
						|
              <IconButton
 | 
						|
                icon={<RestartIcon />}
 | 
						|
                bordered
 | 
						|
                onClick={handleRestartAll}
 | 
						|
                text="Restart All"
 | 
						|
                disabled={isLoading}
 | 
						|
              />
 | 
						|
            </div>
 | 
						|
            <div className="window-action-button">
 | 
						|
              <IconButton
 | 
						|
                icon={<CloseIcon />}
 | 
						|
                bordered
 | 
						|
                onClick={() => navigate(-1)}
 | 
						|
                disabled={isLoading}
 | 
						|
              />
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <div className={styles["mcp-market-page-body"]}>
 | 
						|
          <div className={styles["mcp-market-filter"]}>
 | 
						|
            <input
 | 
						|
              type="text"
 | 
						|
              className={styles["search-bar"]}
 | 
						|
              placeholder={"Search MCP Server"}
 | 
						|
              autoFocus
 | 
						|
              onInput={(e) => setSearchText(e.currentTarget.value)}
 | 
						|
            />
 | 
						|
          </div>
 | 
						|
 | 
						|
          <div className={styles["server-list"]}>{renderServerList()}</div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        {/*编辑服务器配置*/}
 | 
						|
        {editingServerId && (
 | 
						|
          <div className="modal-mask">
 | 
						|
            <Modal
 | 
						|
              title={`Configure Server - ${editingServerId}`}
 | 
						|
              onClose={() => !isLoading && setEditingServerId(undefined)}
 | 
						|
              actions={[
 | 
						|
                <IconButton
 | 
						|
                  key="cancel"
 | 
						|
                  text="Cancel"
 | 
						|
                  onClick={() => setEditingServerId(undefined)}
 | 
						|
                  bordered
 | 
						|
                  disabled={isLoading}
 | 
						|
                />,
 | 
						|
                <IconButton
 | 
						|
                  key="confirm"
 | 
						|
                  text="Save"
 | 
						|
                  type="primary"
 | 
						|
                  onClick={saveServerConfig}
 | 
						|
                  bordered
 | 
						|
                  disabled={isLoading}
 | 
						|
                />,
 | 
						|
              ]}
 | 
						|
            >
 | 
						|
              <List>{renderConfigForm()}</List>
 | 
						|
            </Modal>
 | 
						|
          </div>
 | 
						|
        )}
 | 
						|
 | 
						|
        {viewingServerId && (
 | 
						|
          <div className="modal-mask">
 | 
						|
            <Modal
 | 
						|
              title={`Server Details - ${viewingServerId}`}
 | 
						|
              onClose={() => setViewingServerId(undefined)}
 | 
						|
              actions={[
 | 
						|
                <IconButton
 | 
						|
                  key="close"
 | 
						|
                  text="Close"
 | 
						|
                  onClick={() => setViewingServerId(undefined)}
 | 
						|
                  bordered
 | 
						|
                />,
 | 
						|
              ]}
 | 
						|
            >
 | 
						|
              <div className={styles["tools-list"]}>
 | 
						|
                {isLoading ? (
 | 
						|
                  <div>Loading...</div>
 | 
						|
                ) : tools?.tools ? (
 | 
						|
                  tools.tools.map(
 | 
						|
                    (tool: ListToolsResponse["tools"], index: number) => (
 | 
						|
                      <div key={index} className={styles["tool-item"]}>
 | 
						|
                        <div className={styles["tool-name"]}>{tool.name}</div>
 | 
						|
                        <div className={styles["tool-description"]}>
 | 
						|
                          {tool.description}
 | 
						|
                        </div>
 | 
						|
                      </div>
 | 
						|
                    ),
 | 
						|
                  )
 | 
						|
                ) : (
 | 
						|
                  <div>No tools available</div>
 | 
						|
                )}
 | 
						|
              </div>
 | 
						|
            </Modal>
 | 
						|
          </div>
 | 
						|
        )}
 | 
						|
      </div>
 | 
						|
    </ErrorBoundary>
 | 
						|
  );
 | 
						|
}
 |