Просмотр исходного кода

feat: Optimize MCP configuration logic

Kadxy 1 год назад
Родитель
Сommit
8aa9a500fd

+ 24 - 0
app/components/chat.tsx

@@ -46,6 +46,7 @@ import QualityIcon from "../icons/hd.svg";
 import StyleIcon from "../icons/palette.svg";
 import StyleIcon from "../icons/palette.svg";
 import PluginIcon from "../icons/plugin.svg";
 import PluginIcon from "../icons/plugin.svg";
 import ShortcutkeyIcon from "../icons/shortcutkey.svg";
 import ShortcutkeyIcon from "../icons/shortcutkey.svg";
+import McpToolIcon from "../icons/tool.svg";
 import HeadphoneIcon from "../icons/headphone.svg";
 import HeadphoneIcon from "../icons/headphone.svg";
 import {
 import {
   BOT_HELLO,
   BOT_HELLO,
@@ -121,6 +122,7 @@ import { isEmpty } from "lodash-es";
 import { getModelProvider } from "../utils/model";
 import { getModelProvider } from "../utils/model";
 import { RealtimeChat } from "@/app/components/realtime-chat";
 import { RealtimeChat } from "@/app/components/realtime-chat";
 import clsx from "clsx";
 import clsx from "clsx";
+import { getAvailableClientsCount } from "../mcp/actions";
 
 
 const localStorage = safeLocalStorage();
 const localStorage = safeLocalStorage();
 
 
@@ -130,6 +132,27 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
   loading: () => <LoadingIcon />,
   loading: () => <LoadingIcon />,
 });
 });
 
 
+const MCPAction = () => {
+  const navigate = useNavigate();
+  const [count, setCount] = useState<number>(0);
+
+  useEffect(() => {
+    const loadCount = async () => {
+      const count = await getAvailableClientsCount();
+      setCount(count);
+    };
+    loadCount();
+  }, []);
+
+  return (
+    <ChatAction
+      onClick={() => navigate(Path.McpMarket)}
+      text={`MCP${count ? ` (${count})` : ""}`}
+      icon={<McpToolIcon />}
+    />
+  );
+};
+
 export function SessionConfigModel(props: { onClose: () => void }) {
 export function SessionConfigModel(props: { onClose: () => void }) {
   const chatStore = useChatStore();
   const chatStore = useChatStore();
   const session = chatStore.currentSession();
   const session = chatStore.currentSession();
@@ -799,6 +822,7 @@ export function ChatActions(props: {
             icon={<ShortcutkeyIcon />}
             icon={<ShortcutkeyIcon />}
           />
           />
         )}
         )}
+        {!isMobileScreen && <MCPAction />}
       </>
       </>
       <div className={styles["chat-input-actions-end"]}>
       <div className={styles["chat-input-actions-end"]}>
         {config.realtimeConfig.enable && (
         {config.realtimeConfig.enable && (

+ 10 - 0
app/components/home.tsx

@@ -29,6 +29,8 @@ import { getClientConfig } from "../config/client";
 import { type ClientApi, getClientApi } from "../client/api";
 import { type ClientApi, getClientApi } from "../client/api";
 import { useAccessStore } from "../store";
 import { useAccessStore } from "../store";
 import clsx from "clsx";
 import clsx from "clsx";
+import { initializeMcpSystem } from "../mcp/actions";
+import { showToast } from "./ui-lib";
 
 
 export function Loading(props: { noLogo?: boolean }) {
 export function Loading(props: { noLogo?: boolean }) {
   return (
   return (
@@ -243,6 +245,14 @@ export function Home() {
     useAccessStore.getState().fetch();
     useAccessStore.getState().fetch();
   }, []);
   }, []);
 
 
+  useEffect(() => {
+    // 初始化 MCP 系统
+    initializeMcpSystem().catch((error) => {
+      console.error("Failed to initialize MCP system:", error);
+      showToast("Failed to initialize MCP system");
+    });
+  }, []);
+
   if (!useHasHydrated()) {
   if (!useHasHydrated()) {
     return <Loading />;
     return <Loading />;
   }
   }

+ 86 - 102
app/components/mcp-market.module.scss

@@ -39,8 +39,6 @@
     }
     }
 
 
     .mcp-market-item {
     .mcp-market-item {
-      display: flex;
-      justify-content: space-between;
       padding: 20px;
       padding: 20px;
       border: var(--border-in-light);
       border: var(--border-in-light);
       animation: slide-in ease 0.3s;
       animation: slide-in ease 0.3s;
@@ -68,118 +66,106 @@
 
 
       .mcp-market-header {
       .mcp-market-header {
         display: flex;
         display: flex;
-        align-items: center;
+        justify-content: space-between;
+        align-items: flex-start;
+        width: 100%;
 
 
         .mcp-market-title {
         .mcp-market-title {
-          .mcp-market-name {
-            font-size: 14px;
-            font-weight: bold;
-            display: flex;
+          flex-grow: 1;
+          margin-right: 20px;
+          max-width: calc(100% - 300px);
+        }
+
+        .mcp-market-name {
+          font-size: 14px;
+          font-weight: bold;
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          margin-bottom: 8px;
+
+          .server-status {
+            display: inline-flex;
             align-items: center;
             align-items: center;
-            gap: 8px;
+            margin-left: 10px;
+            padding: 2px 8px;
+            border-radius: 4px;
+            font-size: 12px;
+            background-color: #22c55e;
+            color: #fff;
 
 
-            .server-status {
-              font-size: 12px;
-              padding: 2px 6px;
-              border-radius: 4px;
-              margin-left: 8px;
-              background-color: #10b981;
-              color: white;
-
-              &.error {
-                background-color: #ef4444;
-              }
-
-              &.waiting {
-                background-color: #f59e0b;
-              }
-
-              .error-message {
-                font-size: 11px;
-                opacity: 0.9;
-                margin-left: 4px;
-              }
+            &.error {
+              background-color: #ef4444;
             }
             }
-          }
 
 
-          .mcp-market-info {
-            font-size: 12px;
-            color: var(--black-50);
-            margin-top: 4px;
+            .error-message {
+              margin-left: 4px;
+              font-size: 12px;
+            }
           }
           }
         }
         }
-      }
-
-      .mcp-market-actions {
-        display: flex;
-        gap: 8px;
-        align-items: center;
 
 
-        :global(.icon-button) {
-          transition: all 0.3s ease;
-          border: 1px solid transparent;
+        .repo-link {
+          color: var(--primary);
+          font-size: 12px;
+          display: inline-flex;
+          align-items: center;
+          gap: 4px;
+          text-decoration: none;
+          opacity: 0.8;
+          transition: opacity 0.2s;
 
 
           &:hover {
           &:hover {
-            transform: translateY(-1px);
-            filter: brightness(1.1);
+            opacity: 1;
           }
           }
 
 
-          &.action-primary {
-            background-color: var(--primary);
-            color: white;
-
-            svg {
-              filter: brightness(2);
-            }
-
-            &:hover {
-              background-color: var(--primary);
-              border-color: var(--primary);
-            }
+          svg {
+            width: 14px;
+            height: 14px;
           }
           }
+        }
 
 
-          &.action-warning {
-            background-color: var(--warning);
-            color: white;
+        .tags-container {
+          display: flex;
+          gap: 4px;
+          flex-wrap: wrap;
+          margin-bottom: 8px;
+        }
 
 
-            svg {
-              filter: brightness(2);
-            }
+        .tag {
+          background: var(--gray);
+          color: var(--black);
+          padding: 2px 6px;
+          border-radius: 4px;
+          font-size: 10px;
+          opacity: 0.8;
+        }
 
 
-            &:hover {
-              background-color: var(--warning);
-              border-color: var(--warning);
-            }
-          }
+        .mcp-market-info {
+          color: var(--black);
+          font-size: 12px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        .mcp-market-actions {
+          display: flex;
+          gap: 8px;
+          align-items: flex-start;
+          flex-shrink: 0;
+          min-width: 180px;
+          justify-content: flex-end;
 
 
-          &.action-danger {
-            background-color: transparent;
-            color: var(--danger);
-            border-color: var(--danger);
+          :global(.icon-button) {
+            transition: all 0.3s ease;
+            border: 1px solid transparent;
 
 
             &:hover {
             &:hover {
-              background-color: var(--danger);
-              color: white;
-
-              svg {
-                filter: brightness(2);
-              }
+              transform: translateY(-1px);
+              filter: brightness(1.1);
             }
             }
           }
           }
-
-          &.action-error {
-            color: #ef4444 !important;
-            border-color: #ef4444 !important;
-          }
-        }
-      }
-
-      @media screen and (max-width: 600px) {
-        flex-direction: column;
-        gap: 10px;
-
-        .mcp-market-actions {
-          justify-content: flex-end;
         }
         }
       }
       }
     }
     }
@@ -312,11 +298,6 @@
           outline: none;
           outline: none;
           box-shadow: 0 0 0 2px var(--primary-10);
           box-shadow: 0 0 0 2px var(--primary-10);
         }
         }
-
-        &::placeholder {
-          color: var(--gray-300) !important;
-          opacity: 1;
-        }
       }
       }
 
 
       .browse-button {
       .browse-button {
@@ -534,7 +515,7 @@
     }
     }
   }
   }
 
 
-  .primitives-list {
+  .tools-list {
     display: flex;
     display: flex;
     flex-direction: column;
     flex-direction: column;
     gap: 16px;
     gap: 16px;
@@ -545,11 +526,11 @@
     word-break: break-word;
     word-break: break-word;
     box-sizing: border-box;
     box-sizing: border-box;
 
 
-    .primitive-item {
+    .tool-item {
       width: 100%;
       width: 100%;
       box-sizing: border-box;
       box-sizing: border-box;
 
 
-      .primitive-name {
+      .tool-name {
         font-size: 14px;
         font-size: 14px;
         font-weight: 600;
         font-weight: 600;
         color: var(--black);
         color: var(--black);
@@ -560,7 +541,7 @@
         width: 100%;
         width: 100%;
       }
       }
 
 
-      .primitive-description {
+      .tool-description {
         font-size: 13px;
         font-size: 13px;
         color: var(--gray-500);
         color: var(--gray-500);
         line-height: 1.6;
         line-height: 1.6;
@@ -590,9 +571,12 @@
       border-radius: 10px;
       border-radius: 10px;
       padding: 10px;
       padding: 10px;
       margin-bottom: 10px;
       margin-bottom: 10px;
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
 
 
       .list-header {
       .list-header {
-        margin-bottom: 10px;
+        margin-bottom: 0;
 
 
         .list-title {
         .list-title {
           font-size: 14px;
           font-size: 14px;

+ 324 - 263
app/components/mcp-market.tsx

@@ -7,22 +7,29 @@ import CloseIcon from "../icons/close.svg";
 import DeleteIcon from "../icons/delete.svg";
 import DeleteIcon from "../icons/delete.svg";
 import RestartIcon from "../icons/reload.svg";
 import RestartIcon from "../icons/reload.svg";
 import EyeIcon from "../icons/eye.svg";
 import EyeIcon from "../icons/eye.svg";
+import GithubIcon from "../icons/github.svg";
 import { List, ListItem, Modal, showToast } from "./ui-lib";
 import { List, ListItem, Modal, showToast } from "./ui-lib";
 import { useNavigate } from "react-router-dom";
 import { useNavigate } from "react-router-dom";
-import { useState, useEffect } from "react";
+import { useEffect, useState } from "react";
 import presetServersJson from "../mcp/preset-server.json";
 import presetServersJson from "../mcp/preset-server.json";
-const presetServers = presetServersJson as PresetServer[];
 import {
 import {
-  getMcpConfig,
-  updateMcpConfig,
-  getClientPrimitives,
+  addMcpServer,
+  getClientStatus,
+  getClientTools,
+  getMcpConfigFromFile,
+  removeMcpServer,
   restartAllClients,
   restartAllClients,
-  getClientErrors,
-  refreshClientStatus,
 } from "../mcp/actions";
 } from "../mcp/actions";
-import { McpConfig, PresetServer, ServerConfig } from "../mcp/types";
+import {
+  ListToolsResponse,
+  McpConfigData,
+  PresetServer,
+  ServerConfig,
+} from "../mcp/types";
 import clsx from "clsx";
 import clsx from "clsx";
 
 
+const presetServers = presetServersJson as PresetServer[];
+
 interface ConfigProperty {
 interface ConfigProperty {
   type: string;
   type: string;
   description?: string;
   description?: string;
@@ -33,67 +40,71 @@ interface ConfigProperty {
 export function McpMarketPage() {
 export function McpMarketPage() {
   const navigate = useNavigate();
   const navigate = useNavigate();
   const [searchText, setSearchText] = useState("");
   const [searchText, setSearchText] = useState("");
-  const [config, setConfig] = useState<McpConfig>({ mcpServers: {} });
+  const [userConfig, setUserConfig] = useState<Record<string, any>>({});
   const [editingServerId, setEditingServerId] = useState<string | undefined>();
   const [editingServerId, setEditingServerId] = useState<string | undefined>();
+  const [tools, setTools] = useState<ListToolsResponse["tools"] | null>(null);
   const [viewingServerId, setViewingServerId] = useState<string | undefined>();
   const [viewingServerId, setViewingServerId] = useState<string | undefined>();
-  const [primitives, setPrimitives] = useState<any[]>([]);
-  const [userConfig, setUserConfig] = useState<Record<string, any>>({});
   const [isLoading, setIsLoading] = useState(false);
   const [isLoading, setIsLoading] = useState(false);
-  const [clientErrors, setClientErrors] = useState<
-    Record<string, string | null>
+  const [config, setConfig] = useState<McpConfigData>();
+  const [clientStatuses, setClientStatuses] = useState<
+    Record<
+      string,
+      {
+        status: "active" | "error" | "undefined";
+        errorMsg: string | null;
+      }
+    >
   >({});
   >({});
 
 
-  // 更新服务器状态
-  const updateServerStatus = async () => {
-    await refreshClientStatus();
-    const errors = await getClientErrors();
-    setClientErrors(errors);
+  // 检查服务器是否已添加
+  const isServerAdded = (id: string) => {
+    return id in (config?.mcpServers ?? {});
+  };
+
+  // 获取客户端状态
+  const updateClientStatus = async (clientId: string) => {
+    const status = await getClientStatus(clientId);
+    setClientStatuses((prev) => ({
+      ...prev,
+      [clientId]: status,
+    }));
+    return status;
   };
   };
 
 
-  // 初始加载配置
+  // 从服务器获取初始状态
   useEffect(() => {
   useEffect(() => {
-    const init = async () => {
+    const loadInitialState = async () => {
       try {
       try {
         setIsLoading(true);
         setIsLoading(true);
-        const data = await getMcpConfig();
-        setConfig(data);
-        await updateServerStatus();
+        const config = await getMcpConfigFromFile();
+        setConfig(config);
+
+        // 获取所有客户端的状态
+        const statuses: Record<string, any> = {};
+        for (const clientId of Object.keys(config.mcpServers)) {
+          const status = await getClientStatus(clientId);
+          statuses[clientId] = status;
+        }
+        setClientStatuses(statuses);
       } catch (error) {
       } catch (error) {
-        showToast("Failed to load configuration");
-        console.error(error);
+        console.error("Failed to load initial state:", error);
+        showToast("Failed to load initial state");
       } finally {
       } finally {
         setIsLoading(false);
         setIsLoading(false);
       }
       }
     };
     };
-    init().then();
+    loadInitialState();
   }, []);
   }, []);
 
 
-  // 保存配置
-  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;
-  };
+  // Debug: 监控状态变化
+  useEffect(() => {
+    console.log("MCP Market - Current config:", config);
+    console.log("MCP Market - Current clientStatuses:", clientStatuses);
+  }, [config, clientStatuses]);
 
 
   // 加载当前编辑服务器的配置
   // 加载当前编辑服务器的配置
   useEffect(() => {
   useEffect(() => {
-    if (editingServerId) {
+    if (editingServerId && config) {
       const currentConfig = config.mcpServers[editingServerId];
       const currentConfig = config.mcpServers[editingServerId];
       if (currentConfig) {
       if (currentConfig) {
         // 从当前配置中提取用户配置
         // 从当前配置中提取用户配置
@@ -123,7 +134,7 @@ export function McpMarketPage() {
         setUserConfig({});
         setUserConfig({});
       }
       }
     }
     }
-  }, [editingServerId, config.mcpServers]);
+  }, [editingServerId, config]);
 
 
   // 保存服务器配置
   // 保存服务器配置
   const saveServerConfig = async () => {
   const saveServerConfig = async () => {
@@ -131,6 +142,7 @@ export function McpMarketPage() {
     if (!preset || !preset.configSchema || !editingServerId) return;
     if (!preset || !preset.configSchema || !editingServerId) return;
 
 
     try {
     try {
+      setIsLoading(true);
       // 构建服务器配置
       // 构建服务器配置
       const args = [...preset.baseArgs];
       const args = [...preset.baseArgs];
       const env: Record<string, string> = {};
       const env: Record<string, string> = {};
@@ -160,22 +172,113 @@ export function McpMarketPage() {
         ...(Object.keys(env).length > 0 ? { env } : {}),
         ...(Object.keys(env).length > 0 ? { env } : {}),
       };
       };
 
 
-      // 更新配置
-      const newConfig = {
-        ...config,
-        mcpServers: {
-          ...config.mcpServers,
-          [editingServerId]: serverConfig,
-        },
-      };
+      // 更新配置并初始化新服务器
+      const newConfig = await addMcpServer(editingServerId, serverConfig);
+      setConfig(newConfig);
+
+      // 更新状态
+      const status = await getClientStatus(editingServerId);
+      setClientStatuses((prev) => ({
+        ...prev,
+        [editingServerId]: status,
+      }));
 
 
-      await saveConfig(newConfig);
       setEditingServerId(undefined);
       setEditingServerId(undefined);
       showToast("Server configuration saved successfully");
       showToast("Server configuration saved successfully");
     } catch (error) {
     } catch (error) {
       showToast(
       showToast(
         error instanceof Error ? error.message : "Failed to save configuration",
         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<string, any> = {};
+      for (const clientId of Object.keys(newConfig.mcpServers)) {
+        const status = await getClientStatus(clientId);
+        statuses[clientId] = status;
+      }
+      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);
     }
     }
   };
   };
 
 
@@ -188,8 +291,17 @@ export function McpMarketPage() {
       ([key, prop]: [string, ConfigProperty]) => {
       ([key, prop]: [string, ConfigProperty]) => {
         if (prop.type === "array") {
         if (prop.type === "array") {
           const currentValue = userConfig[key as keyof typeof userConfig] || [];
           const currentValue = userConfig[key as keyof typeof userConfig] || [];
+          const itemLabel = (prop as any).itemLabel || key;
+          const addButtonText =
+            (prop as any).addButtonText || `Add ${itemLabel}`;
+
           return (
           return (
-            <ListItem key={key} title={key} subTitle={prop.description}>
+            <ListItem
+              key={key}
+              title={key}
+              subTitle={prop.description}
+              vertical
+            >
               <div className={styles["path-list"]}>
               <div className={styles["path-list"]}>
                 {(currentValue as string[]).map(
                 {(currentValue as string[]).map(
                   (value: string, index: number) => (
                   (value: string, index: number) => (
@@ -197,7 +309,7 @@ export function McpMarketPage() {
                       <input
                       <input
                         type="text"
                         type="text"
                         value={value}
                         value={value}
-                        placeholder={`Path ${index + 1}`}
+                        placeholder={`${itemLabel} ${index + 1}`}
                         onChange={(e) => {
                         onChange={(e) => {
                           const newValue = [...currentValue] as string[];
                           const newValue = [...currentValue] as string[];
                           newValue[index] = e.target.value;
                           newValue[index] = e.target.value;
@@ -218,7 +330,7 @@ export function McpMarketPage() {
                 )}
                 )}
                 <IconButton
                 <IconButton
                   icon={<AddIcon />}
                   icon={<AddIcon />}
-                  text="Add Path"
+                  text={addButtonText}
                   className={styles["add-button"]}
                   className={styles["add-button"]}
                   bordered
                   bordered
                   onClick={() => {
                   onClick={() => {
@@ -251,83 +363,146 @@ export function McpMarketPage() {
     );
     );
   };
   };
 
 
-  // 获取服务器的 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 checkServerStatus = (clientId: string) => {
+    return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
   };
   };
 
 
-  // 重启所有客户端
-  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 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 addServer = async (preset: PresetServer) => {
-    if (!preset.configurable) {
-      try {
-        setIsLoading(true);
-        showToast("Creating MCP client...");
-        // 如果服务器不需要配置,直接添加
-        const serverConfig: ServerConfig = {
-          command: preset.command,
-          args: [...preset.baseArgs],
+        // 定义状态优先级
+        const statusPriority = {
+          error: 0,
+          active: 1,
+          undefined: 2,
         };
         };
-        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);
-    }
+        // 首先按状态排序
+        if (aStatus !== bStatus) {
+          return statusPriority[aStatus] - statusPriority[bStatus];
+        }
+
+        // 然后按名称排序
+        return a.name.localeCompare(b.name);
+      })
+      .map((server) => (
+        <div
+          className={clsx(styles["mcp-market-item"], {
+            [styles["disabled"]]: isLoading,
+          })}
+          key={server.id}
+        >
+          <div className={styles["mcp-market-header"]}>
+            <div className={styles["mcp-market-title"]}>
+              <div className={styles["mcp-market-name"]}>
+                {server.name}
+                {checkServerStatus(server.id).status !== "undefined" && (
+                  <span
+                    className={clsx(styles["server-status"], {
+                      [styles["error"]]:
+                        checkServerStatus(server.id).status === "error",
+                    })}
+                  >
+                    {checkServerStatus(server.id).status === "error" ? (
+                      <>
+                        Error
+                        <span className={styles["error-message"]}>
+                          : {checkServerStatus(server.id).errorMsg}
+                        </span>
+                      </>
+                    ) : (
+                      "Active"
+                    )}
+                  </span>
+                )}
+                {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"
+                      className={clsx({
+                        [styles["action-error"]]:
+                          checkServerStatus(server.id).status === "error",
+                      })}
+                      onClick={() => setEditingServerId(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={<DeleteIcon />}
+                    text="Remove"
+                    className={styles["action-danger"]}
+                    onClick={() => removeServer(server.id)}
+                    disabled={isLoading}
+                  />
+                </>
+              ) : (
+                <IconButton
+                  icon={<AddIcon />}
+                  text="Add"
+                  className={styles["action-primary"]}
+                  onClick={() => addServer(server)}
+                  disabled={isLoading}
+                />
+              )}
+            </div>
+          </div>
+        </div>
+      ));
   };
   };
 
 
   return (
   return (
@@ -342,7 +517,7 @@ export function McpMarketPage() {
               )}
               )}
             </div>
             </div>
             <div className="window-header-sub-title">
             <div className="window-header-sub-title">
-              {Object.keys(config.mcpServers).length} servers configured
+              {Object.keys(config?.mcpServers ?? {}).length} servers configured
             </div>
             </div>
           </div>
           </div>
 
 
@@ -351,7 +526,7 @@ export function McpMarketPage() {
               <IconButton
               <IconButton
                 icon={<RestartIcon />}
                 icon={<RestartIcon />}
                 bordered
                 bordered
-                onClick={handleRestart}
+                onClick={handleRestartAll}
                 text="Restart All"
                 text="Restart All"
                 disabled={isLoading}
                 disabled={isLoading}
               />
               />
@@ -378,121 +553,10 @@ export function McpMarketPage() {
             />
             />
           </div>
           </div>
 
 
-          <div className={styles["server-list"]}>
-            {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) => (
-                <div
-                  className={clsx(styles["mcp-market-item"], {
-                    [styles["disabled"]]: isLoading,
-                  })}
-                  key={server.id}
-                >
-                  <div className={styles["mcp-market-header"]}>
-                    <div className={styles["mcp-market-title"]}>
-                      <div className={styles["mcp-market-name"]}>
-                        {server.name}
-                        {isServerAdded(server.id) && (
-                          <span
-                            className={clsx(styles["server-status"], {
-                              [styles["error"]]:
-                                clientErrors[server.id] !== null,
-                            })}
-                          >
-                            {clientErrors[server.id] === null
-                              ? "Active"
-                              : "Error"}
-                            {clientErrors[server.id] && (
-                              <span className={styles["error-message"]}>
-                                : {clientErrors[server.id]}
-                              </span>
-                            )}
-                          </span>
-                        )}
-                      </div>
-                      <div
-                        className={clsx(styles["mcp-market-info"], "one-line")}
-                      >
-                        {server.description}
-                      </div>
-                    </div>
-                  </div>
-                  <div className={styles["mcp-market-actions"]}>
-                    {isServerAdded(server.id) ? (
-                      <>
-                        {server.configurable && (
-                          <IconButton
-                            icon={<EditIcon />}
-                            text="Configure"
-                            className={clsx({
-                              [styles["action-error"]]:
-                                clientErrors[server.id] !== null,
-                            })}
-                            onClick={() => setEditingServerId(server.id)}
-                            disabled={isLoading}
-                          />
-                        )}
-                        {isServerAdded(server.id) && (
-                          <IconButton
-                            icon={<EyeIcon />}
-                            text="Tools"
-                            onClick={async () => {
-                              if (clientErrors[server.id] !== null) {
-                                showToast("Server is not running");
-                                return;
-                              }
-                              setViewingServerId(server.id);
-                              await loadPrimitives(server.id);
-                            }}
-                            disabled={isLoading}
-                          />
-                        )}
-                        <IconButton
-                          icon={<DeleteIcon />}
-                          text="Remove"
-                          className={styles["action-danger"]}
-                          onClick={() => removeServer(server.id)}
-                          disabled={isLoading}
-                        />
-                      </>
-                    ) : (
-                      <IconButton
-                        icon={<AddIcon />}
-                        text="Add"
-                        className={styles["action-primary"]}
-                        onClick={() => addServer(server)}
-                        disabled={isLoading}
-                      />
-                    )}
-                  </div>
-                </div>
-              ))}
-          </div>
+          <div className={styles["server-list"]}>{renderServerList()}</div>
         </div>
         </div>
 
 
+        {/*编辑服务器配置*/}
         {editingServerId && (
         {editingServerId && (
           <div className="modal-mask">
           <div className="modal-mask">
             <Modal
             <Modal
@@ -521,6 +585,7 @@ export function McpMarketPage() {
           </div>
           </div>
         )}
         )}
 
 
+        {/*支持的Tools*/}
         {viewingServerId && (
         {viewingServerId && (
           <div className="modal-mask">
           <div className="modal-mask">
             <Modal
             <Modal
@@ -535,24 +600,20 @@ export function McpMarketPage() {
                 />,
                 />,
               ]}
               ]}
             >
             >
-              <div className={styles["primitives-list"]}>
+              <div className={styles["tools-list"]}>
                 {isLoading ? (
                 {isLoading ? (
                   <div>Loading...</div>
                   <div>Loading...</div>
-                ) : primitives.filter((p) => p.type === "tool").length > 0 ? (
-                  primitives
-                    .filter((p) => p.type === "tool")
-                    .map((primitive, index) => (
-                      <div key={index} className={styles["primitive-item"]}>
-                        <div className={styles["primitive-name"]}>
-                          {primitive.value.name}
+                ) : 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>
-                        {primitive.value.description && (
-                          <div className={styles["primitive-description"]}>
-                            {primitive.value.description}
-                          </div>
-                        )}
                       </div>
                       </div>
-                    ))
+                    ),
+                  )
                 ) : (
                 ) : (
                   <div>No tools available</div>
                   <div>No tools available</div>
                 )}
                 )}

+ 6 - 5
app/constant.ts

@@ -88,6 +88,7 @@ export enum StoreKey {
   Update = "chat-update",
   Update = "chat-update",
   Sync = "sync",
   Sync = "sync",
   SdList = "sd-list",
   SdList = "sd-list",
+  Mcp = "mcp-store",
 }
 }
 
 
 export const DEFAULT_SIDEBAR_WIDTH = 300;
 export const DEFAULT_SIDEBAR_WIDTH = 300;
@@ -254,18 +255,18 @@ Latex inline: \\(x^2\\)
 Latex block: $$e=mc^2$$
 Latex block: $$e=mc^2$$
 `;
 `;
 
 
-export const MCP_PRIMITIVES_TEMPLATE = `
+export const MCP_TOOLS_TEMPLATE = `
 [clientId]
 [clientId]
 {{ clientId }}
 {{ clientId }}
-[primitives]
-{{ primitives }}
+[tools]
+{{ tools }}
 `;
 `;
 
 
 export const MCP_SYSTEM_TEMPLATE = `
 export const MCP_SYSTEM_TEMPLATE = `
 You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed.
 You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed.
 
 
-1. TOOLS AVAILABLE:
-{{ MCP_PRIMITIVES }}
+1. AVAILABLE TOOLS:
+{{ MCP_TOOLS }}
 
 
 2. WHEN TO USE TOOLS:
 2. WHEN TO USE TOOLS:
    - ALWAYS USE TOOLS when they can help answer user questions
    - ALWAYS USE TOOLS when they can help answer user questions

Разница между файлами не показана из-за своего большого размера
+ 2 - 0
app/icons/tool.svg


+ 158 - 177
app/mcp/actions.ts

@@ -1,236 +1,217 @@
 "use server";
 "use server";
-import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 import {
 import {
   createClient,
   createClient,
   executeRequest,
   executeRequest,
-  listPrimitives,
-  Primitive,
+  listTools,
+  removeClient,
 } from "./client";
 } from "./client";
 import { MCPClientLogger } from "./logger";
 import { MCPClientLogger } from "./logger";
-import { McpRequestMessage, McpConfig, ServerConfig } from "./types";
+import {
+  DEFAULT_MCP_CONFIG,
+  McpClientData,
+  McpConfigData,
+  McpRequestMessage,
+  ServerConfig,
+} from "./types";
 import fs from "fs/promises";
 import fs from "fs/promises";
 import path from "path";
 import path from "path";
 
 
 const logger = new MCPClientLogger("MCP Actions");
 const logger = new MCPClientLogger("MCP Actions");
+const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
 
 
-// Use Map to store all clients
-const clientsMap = new Map<
-  string,
-  { client: Client | null; primitives: Primitive[]; errorMsg: string | null }
->();
+const clientsMap = new Map<string, McpClientData>();
 
 
-// Whether initialized
-let initialized = false;
+// 获取客户端状态
+export async function getClientStatus(clientId: string) {
+  const status = clientsMap.get(clientId);
+  if (!status) return { status: "undefined" as const, errorMsg: null };
 
 
-// Store failed clients
-let errorClients: string[] = [];
+  return {
+    status: status.errorMsg ? ("error" as const) : ("active" as const),
+    errorMsg: status.errorMsg,
+  };
+}
 
 
-const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
+// 获取客户端工具
+export async function getClientTools(clientId: string) {
+  return clientsMap.get(clientId)?.tools ?? null;
+}
 
 
-// 获取 MCP 配置
-export async function getMcpConfig(): Promise<McpConfig> {
-  try {
-    const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
-    return JSON.parse(configStr);
-  } catch (error) {
-    console.error("Failed to read MCP config:", error);
-    return { mcpServers: {} };
+// 获取可用客户端数量
+export async function getAvailableClientsCount() {
+  let count = 0;
+  clientsMap.forEach((map) => {
+    if (!map.errorMsg) {
+      count += map?.tools?.tools?.length ?? 0;
+    }
+  });
+  return count;
+}
+
+// 获取所有客户端工具
+export async function getAllTools() {
+  const result = [];
+  for (const [clientId, status] of clientsMap.entries()) {
+    result.push({
+      clientId,
+      tools: status.tools,
+    });
   }
   }
+  return result;
 }
 }
 
 
-// 更新 MCP 配置
-export async function updateMcpConfig(config: McpConfig): Promise<void> {
+// 初始化单个客户端
+async function initializeSingleClient(
+  clientId: string,
+  serverConfig: ServerConfig,
+) {
+  logger.info(`Initializing client [${clientId}]...`);
   try {
   try {
-    await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
+    const client = await createClient(clientId, serverConfig);
+    const tools = await listTools(client);
+    clientsMap.set(clientId, { client, tools, errorMsg: null });
+    logger.success(`Client [${clientId}] initialized successfully`);
   } catch (error) {
   } catch (error) {
-    console.error("Failed to write MCP config:", error);
-    throw error;
+    clientsMap.set(clientId, {
+      client: null,
+      tools: null,
+      errorMsg: error instanceof Error ? error.message : String(error),
+    });
+    logger.error(`Failed to initialize client [${clientId}]: ${error}`);
   }
   }
 }
 }
 
 
-// 重新初始化所有客户端
-export async function reinitializeMcpClients() {
-  logger.info("Reinitializing MCP clients...");
-  // 遍历所有客户端,关闭
+// 初始化系统
+export async function initializeMcpSystem() {
+  logger.info("MCP Actions starting...");
   try {
   try {
-    for (const [clientId, clientData] of clientsMap.entries()) {
-      clientData.client?.close();
+    const config = await getMcpConfigFromFile();
+    // 初始化所有客户端
+    for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
+      await initializeSingleClient(clientId, serverConfig);
     }
     }
+    return config;
   } catch (error) {
   } catch (error) {
-    logger.error(`Failed to close clients: ${error}`);
+    logger.error(`Failed to initialize MCP system: ${error}`);
+    throw error;
   }
   }
-  // 清空状态
-  clientsMap.clear();
-  errorClients = [];
-  initialized = false;
-  // 重新初始化
-  return initializeMcpClients();
 }
 }
 
 
-// Initialize all configured clients
-export async function initializeMcpClients() {
-  // If already initialized, return
-  if (initialized) {
-    return { errorClients };
+// 添加服务器
+export async function addMcpServer(clientId: string, config: ServerConfig) {
+  try {
+    const currentConfig = await getMcpConfigFromFile();
+    const newConfig = {
+      ...currentConfig,
+      mcpServers: {
+        ...currentConfig.mcpServers,
+        [clientId]: config,
+      },
+    };
+    await updateMcpConfig(newConfig);
+    // 只初始化新添加的服务器
+    await initializeSingleClient(clientId, config);
+    return newConfig;
+  } catch (error) {
+    logger.error(`Failed to add server [${clientId}]: ${error}`);
+    throw error;
   }
   }
+}
 
 
-  logger.info("Starting to initialize MCP clients...");
-  errorClients = [];
-
-  const config = await getMcpConfig();
-  // Initialize all clients, key is clientId, value is client config
-  for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
-    try {
-      logger.info(`Initializing MCP client: ${clientId}`);
-      const client = await createClient(serverConfig as ServerConfig, clientId);
-      const primitives = await listPrimitives(client);
-      clientsMap.set(clientId, { client, primitives, errorMsg: null });
-      logger.success(
-        `Client [${clientId}] initialized, ${primitives.length} primitives supported`,
-      );
-    } catch (error) {
-      errorClients.push(clientId);
-      clientsMap.set(clientId, {
-        client: null,
-        primitives: [],
-        errorMsg: error instanceof Error ? error.message : String(error),
-      });
-      logger.error(`Failed to initialize client ${clientId}: ${error}`);
+// 移除服务器
+export async function removeMcpServer(clientId: string) {
+  try {
+    const currentConfig = await getMcpConfigFromFile();
+    const { [clientId]: _, ...rest } = currentConfig.mcpServers;
+    const newConfig = {
+      ...currentConfig,
+      mcpServers: rest,
+    };
+    await updateMcpConfig(newConfig);
+
+    // 关闭并移除客户端
+    const client = clientsMap.get(clientId);
+    if (client?.client) {
+      await removeClient(client.client);
     }
     }
-  }
-
-  initialized = true;
+    clientsMap.delete(clientId);
 
 
-  if (errorClients.length > 0) {
-    logger.warn(`Failed to initialize clients: ${errorClients.join(", ")}`);
-  } else {
-    logger.success("All MCP clients initialized");
+    return newConfig;
+  } catch (error) {
+    logger.error(`Failed to remove server [${clientId}]: ${error}`);
+    throw error;
   }
   }
+}
 
 
-  const availableClients = await getAvailableClients();
-  logger.info(`Available clients: ${availableClients.join(",")}`);
+// 重启所有客户端
+export async function restartAllClients() {
+  logger.info("Restarting all clients...");
+  try {
+    // 关闭所有客户端
+    for (const client of clientsMap.values()) {
+      if (client.client) {
+        await removeClient(client.client);
+      }
+    }
+    // 清空状态
+    clientsMap.clear();
 
 
-  return { errorClients };
+    // 重新初始化
+    const config = await getMcpConfigFromFile();
+    for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
+      await initializeSingleClient(clientId, serverConfig);
+    }
+    return config;
+  } catch (error) {
+    logger.error(`Failed to restart clients: ${error}`);
+    throw error;
+  }
 }
 }
 
 
-// Execute MCP request
+// 执行 MCP 请求
 export async function executeMcpAction(
 export async function executeMcpAction(
   clientId: string,
   clientId: string,
   request: McpRequestMessage,
   request: McpRequestMessage,
 ) {
 ) {
   try {
   try {
-    // Find the corresponding client
-    const client = clientsMap.get(clientId)?.client;
-    if (!client) {
-      logger.error(`Client ${clientId} not found`);
-      return;
+    const client = clientsMap.get(clientId);
+    if (!client?.client) {
+      throw new Error(`Client ${clientId} not found`);
     }
     }
-
-    logger.info(`Executing MCP request for ${clientId}`);
-
-    // Execute request and return result
-    return await executeRequest(client, request);
+    logger.info(`Executing request for [${clientId}]`);
+    return await executeRequest(client.client, request);
   } catch (error) {
   } catch (error) {
-    logger.error(`MCP execution error: ${error}`);
+    logger.error(`Failed to execute request for [${clientId}]: ${error}`);
     throw error;
     throw error;
   }
   }
 }
 }
 
 
-// Get all available client IDs
-export async function getAvailableClients() {
-  return Array.from(clientsMap.entries())
-    .filter(([_, data]) => data.errorMsg === null)
-    .map(([clientId]) => clientId);
-}
-
-// Get all primitives from all clients
-export async function getAllPrimitives(): Promise<
-  {
-    clientId: string;
-    primitives: Primitive[];
-  }[]
-> {
-  return Array.from(clientsMap.entries()).map(([clientId, { primitives }]) => ({
-    clientId,
-    primitives,
-  }));
-}
-
-// 获取客户端的 Primitives
-export async function getClientPrimitives(clientId: string) {
+// 获取 MCP 配置文件
+export async function getMcpConfigFromFile(): Promise<McpConfigData> {
   try {
   try {
-    const clientData = clientsMap.get(clientId);
-    if (!clientData) {
-      console.warn(`Client ${clientId} not found in map`);
-      return null;
-    }
-    if (clientData.errorMsg) {
-      console.warn(`Client ${clientId} has error: ${clientData.errorMsg}`);
-      return null;
-    }
-    return clientData.primitives;
+    const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
+    return JSON.parse(configStr);
   } catch (error) {
   } catch (error) {
-    console.error(`Failed to get primitives for client ${clientId}:`, error);
-    return null;
-  }
-}
-
-// 重启所有客户端
-export async function restartAllClients() {
-  logger.info("Restarting all MCP clients...");
-
-  // 清空状态
-  clientsMap.clear();
-  errorClients = [];
-  initialized = false;
-
-  // 重新初始化
-  await initializeMcpClients();
-
-  return {
-    success: errorClients.length === 0,
-    errorClients,
-  };
-}
-
-// 获取所有客户端状态
-export async function getAllClientStatus(): Promise<
-  Record<string, string | null>
-> {
-  const status: Record<string, string | null> = {};
-  for (const [clientId, data] of clientsMap.entries()) {
-    status[clientId] = data.errorMsg;
+    logger.error(`Failed to load MCP config, using default config: ${error}`);
+    return DEFAULT_MCP_CONFIG;
   }
   }
-  return status;
 }
 }
 
 
-// 检查客户端状态
-export async function getClientErrors(): Promise<
-  Record<string, string | null>
-> {
-  const errors: Record<string, string | null> = {};
-  for (const [clientId, data] of clientsMap.entries()) {
-    errors[clientId] = data.errorMsg;
+// 更新 MCP 配置文件
+async function updateMcpConfig(config: McpConfigData): Promise<void> {
+  try {
+    await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
+  } catch (error) {
+    throw error;
   }
   }
-  return errors;
 }
 }
 
 
-// 获取客户端状态,不重新初始化
-export async function refreshClientStatus() {
-  logger.info("Refreshing client status...");
-
-  // 如果还没初始化过,则初始化
-  if (!initialized) {
-    return initializeMcpClients();
+// 重新初始化单个客户端
+export async function reinitializeClient(clientId: string) {
+  const config = await getMcpConfigFromFile();
+  const serverConfig = config.mcpServers[clientId];
+  if (!serverConfig) {
+    throw new Error(`Server config not found for client ${clientId}`);
   }
   }
-
-  // 否则只更新错误状态
-  errorClients = [];
-  for (const [clientId, clientData] of clientsMap.entries()) {
-    if (clientData.errorMsg !== null) {
-      errorClients.push(clientId);
-    }
-  }
-
-  return { errorClients };
+  await initializeSingleClient(clientId, serverConfig);
 }
 }

+ 15 - 55
app/mcp/client.ts

@@ -1,85 +1,45 @@
 import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 import { MCPClientLogger } from "./logger";
 import { MCPClientLogger } from "./logger";
-import { McpRequestMessage } from "./types";
+import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types";
 import { z } from "zod";
 import { z } from "zod";
 
 
-export interface ServerConfig {
-  command: string;
-  args?: string[];
-  env?: Record<string, string>;
-}
-
 const logger = new MCPClientLogger();
 const logger = new MCPClientLogger();
 
 
 export async function createClient(
 export async function createClient(
-  serverConfig: ServerConfig,
-  name: string,
+  id: string,
+  config: ServerConfig,
 ): Promise<Client> {
 ): Promise<Client> {
-  logger.info(`Creating client for server ${name}`);
+  logger.info(`Creating client for ${id}...`);
 
 
   const transport = new StdioClientTransport({
   const transport = new StdioClientTransport({
-    command: serverConfig.command,
-    args: serverConfig.args,
-    env: serverConfig.env,
+    command: config.command,
+    args: config.args,
+    env: config.env,
   });
   });
+
   const client = new Client(
   const client = new Client(
     {
     {
-      name: `nextchat-mcp-client-${name}`,
+      name: `nextchat-mcp-client-${id}`,
       version: "1.0.0",
       version: "1.0.0",
     },
     },
     {
     {
-      capabilities: {
-        // roots: {
-        //   listChanged: true,
-        // },
-      },
+      capabilities: {},
     },
     },
   );
   );
   await client.connect(transport);
   await client.connect(transport);
   return client;
   return client;
 }
 }
 
 
-export interface Primitive {
-  type: "resource" | "tool" | "prompt";
-  value: any;
+export async function removeClient(client: Client) {
+  logger.info(`Removing client...`);
+  await client.close();
 }
 }
 
 
-/** List all resources, tools, and prompts */
-export async function listPrimitives(client: Client): Promise<Primitive[]> {
-  const capabilities = client.getServerCapabilities();
-  const primitives: Primitive[] = [];
-  const promises = [];
-  if (capabilities?.resources) {
-    promises.push(
-      client.listResources().then(({ resources }) => {
-        resources.forEach((item) =>
-          primitives.push({ type: "resource", value: item }),
-        );
-      }),
-    );
-  }
-  if (capabilities?.tools) {
-    promises.push(
-      client.listTools().then(({ tools }) => {
-        tools.forEach((item) => primitives.push({ type: "tool", value: item }));
-      }),
-    );
-  }
-  if (capabilities?.prompts) {
-    promises.push(
-      client.listPrompts().then(({ prompts }) => {
-        prompts.forEach((item) =>
-          primitives.push({ type: "prompt", value: item }),
-        );
-      }),
-    );
-  }
-  await Promise.all(promises);
-  return primitives;
+export async function listTools(client: Client): Promise<ListToolsResponse> {
+  return client.listTools();
 }
 }
 
 
-/** Execute a request */
 export async function executeRequest(
 export async function executeRequest(
   client: Client,
   client: Client,
   request: McpRequestMessage,
   request: McpRequestMessage,

+ 5 - 9
app/mcp/example.ts

@@ -1,27 +1,23 @@
-import { createClient, listPrimitives } from "@/app/mcp/client";
+import { createClient, listTools } from "@/app/mcp/client";
 import { MCPClientLogger } from "@/app/mcp/logger";
 import { MCPClientLogger } from "@/app/mcp/logger";
 import conf from "./mcp_config.json";
 import conf from "./mcp_config.json";
 
 
 const logger = new MCPClientLogger("MCP Server Example", true);
 const logger = new MCPClientLogger("MCP Server Example", true);
 
 
-const TEST_SERVER = "everything";
+const TEST_SERVER = "filesystem";
 
 
 async function main() {
 async function main() {
   logger.info(`All MCP servers: ${Object.keys(conf.mcpServers).join(", ")}`);
   logger.info(`All MCP servers: ${Object.keys(conf.mcpServers).join(", ")}`);
 
 
   logger.info(`Connecting to server ${TEST_SERVER}...`);
   logger.info(`Connecting to server ${TEST_SERVER}...`);
 
 
-  const client = await createClient(conf.mcpServers[TEST_SERVER], TEST_SERVER);
-  const primitives = await listPrimitives(client);
+  const client = await createClient(TEST_SERVER, conf.mcpServers[TEST_SERVER]);
+  const tools = await listTools(client);
 
 
   logger.success(`Connected to server ${TEST_SERVER}`);
   logger.success(`Connected to server ${TEST_SERVER}`);
 
 
   logger.info(
   logger.info(
-    `${TEST_SERVER} supported primitives:\n${JSON.stringify(
-      primitives.filter((i) => i.type === "tool"),
-      null,
-      2,
-    )}`,
+    `${TEST_SERVER} supported primitives:\n${JSON.stringify(tools, null, 2)}`,
   );
   );
 }
 }
 
 

+ 11 - 2
app/mcp/mcp_config.json

@@ -1,3 +1,12 @@
 {
 {
-  "mcpServers": {}
-}
+  "mcpServers": {
+    "filesystem": {
+      "command": "npx",
+      "args": [
+        "-y",
+        "@modelcontextprotocol/server-filesystem",
+        "."
+      ]
+    }
+  }
+}

+ 24 - 2
app/mcp/preset-server.json

@@ -2,7 +2,9 @@
   {
   {
     "id": "filesystem",
     "id": "filesystem",
     "name": "Filesystem",
     "name": "Filesystem",
-    "description": "Secure file operations with configurable access controls",
+    "description": "Secure file operations with configurable access controlsSecure file operations with configurable access controlsSecure file operations with configurable access controlsSecure file operations with configurable access controls",
+    "repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem",
+    "tags": ["filesystem", "storage", "local"],
     "command": "npx",
     "command": "npx",
     "baseArgs": ["-y", "@modelcontextprotocol/server-filesystem"],
     "baseArgs": ["-y", "@modelcontextprotocol/server-filesystem"],
     "configurable": true,
     "configurable": true,
@@ -12,7 +14,9 @@
           "type": "array",
           "type": "array",
           "description": "Allowed file system paths",
           "description": "Allowed file system paths",
           "required": true,
           "required": true,
-          "minItems": 1
+          "minItems": 1,
+          "itemLabel": "Path",
+          "addButtonText": "Add Path"
         }
         }
       }
       }
     },
     },
@@ -27,6 +31,8 @@
     "id": "github",
     "id": "github",
     "name": "GitHub",
     "name": "GitHub",
     "description": "Repository management, file operations, and GitHub API integration",
     "description": "Repository management, file operations, and GitHub API integration",
+    "repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/github",
+    "tags": ["github", "git", "api", "vcs"],
     "command": "npx",
     "command": "npx",
     "baseArgs": ["-y", "@modelcontextprotocol/server-github"],
     "baseArgs": ["-y", "@modelcontextprotocol/server-github"],
     "configurable": true,
     "configurable": true,
@@ -50,6 +56,8 @@
     "id": "gdrive",
     "id": "gdrive",
     "name": "Google Drive",
     "name": "Google Drive",
     "description": "File access and search capabilities for Google Drive",
     "description": "File access and search capabilities for Google Drive",
+    "repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive",
+    "tags": ["google", "drive", "storage", "cloud"],
     "command": "npx",
     "command": "npx",
     "baseArgs": ["-y", "@modelcontextprotocol/server-gdrive"],
     "baseArgs": ["-y", "@modelcontextprotocol/server-gdrive"],
     "configurable": false
     "configurable": false
@@ -58,6 +66,8 @@
     "id": "playwright",
     "id": "playwright",
     "name": "Playwright",
     "name": "Playwright",
     "description": "Browser automation and webscrapping with Playwright",
     "description": "Browser automation and webscrapping with Playwright",
+    "repo": "https://github.com/executeautomation/mcp-playwright",
+    "tags": ["browser", "automation", "scraping"],
     "command": "npx",
     "command": "npx",
     "baseArgs": ["-y", "@executeautomation/playwright-mcp-server"],
     "baseArgs": ["-y", "@executeautomation/playwright-mcp-server"],
     "configurable": false
     "configurable": false
@@ -66,6 +76,8 @@
     "id": "mongodb",
     "id": "mongodb",
     "name": "MongoDB",
     "name": "MongoDB",
     "description": "Direct interaction with MongoDB databases",
     "description": "Direct interaction with MongoDB databases",
+    "repo": "",
+    "tags": ["database", "mongodb", "nosql"],
     "command": "node",
     "command": "node",
     "baseArgs": ["dist/index.js"],
     "baseArgs": ["dist/index.js"],
     "configurable": true,
     "configurable": true,
@@ -89,6 +101,8 @@
     "id": "difyworkflow",
     "id": "difyworkflow",
     "name": "Dify Workflow",
     "name": "Dify Workflow",
     "description": "Tools to query and execute Dify workflows",
     "description": "Tools to query and execute Dify workflows",
+    "repo": "https://github.com/gotoolkits/mcp-difyworkflow-server",
+    "tags": ["workflow", "automation", "dify"],
     "command": "mcp-difyworkflow-server",
     "command": "mcp-difyworkflow-server",
     "baseArgs": ["-base-url"],
     "baseArgs": ["-base-url"],
     "configurable": true,
     "configurable": true,
@@ -130,6 +144,8 @@
     "id": "postgres",
     "id": "postgres",
     "name": "PostgreSQL",
     "name": "PostgreSQL",
     "description": "Read-only database access with schema inspection",
     "description": "Read-only database access with schema inspection",
+    "repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres",
+    "tags": ["database", "postgresql", "sql"],
     "command": "docker",
     "command": "docker",
     "baseArgs": ["run", "-i", "--rm", "mcp/postgres"],
     "baseArgs": ["run", "-i", "--rm", "mcp/postgres"],
     "configurable": true,
     "configurable": true,
@@ -153,6 +169,8 @@
     "id": "brave-search",
     "id": "brave-search",
     "name": "Brave Search",
     "name": "Brave Search",
     "description": "Web and local search using Brave's Search API",
     "description": "Web and local search using Brave's Search API",
+    "repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search",
+    "tags": ["search", "brave", "api"],
     "command": "npx",
     "command": "npx",
     "baseArgs": ["-y", "@modelcontextprotocol/server-brave-search"],
     "baseArgs": ["-y", "@modelcontextprotocol/server-brave-search"],
     "configurable": true,
     "configurable": true,
@@ -176,6 +194,8 @@
     "id": "google-maps",
     "id": "google-maps",
     "name": "Google Maps",
     "name": "Google Maps",
     "description": "Location services, directions, and place details",
     "description": "Location services, directions, and place details",
+    "repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps",
+    "tags": ["maps", "google", "location", "api"],
     "command": "npx",
     "command": "npx",
     "baseArgs": ["-y", "@modelcontextprotocol/server-google-maps"],
     "baseArgs": ["-y", "@modelcontextprotocol/server-google-maps"],
     "configurable": true,
     "configurable": true,
@@ -199,6 +219,8 @@
     "id": "docker-mcp",
     "id": "docker-mcp",
     "name": "Docker",
     "name": "Docker",
     "description": "Run and manage docker containers, docker compose, and logs",
     "description": "Run and manage docker containers, docker compose, and logs",
+    "repo": "https://github.com/QuantGeekDev/docker-mcp",
+    "tags": ["docker", "container", "devops"],
     "command": "uvx",
     "command": "uvx",
     "baseArgs": ["docker-mcp"],
     "baseArgs": ["docker-mcp"],
     "configurable": false
     "configurable": false

+ 59 - 1
app/mcp/types.ts

@@ -1,6 +1,7 @@
 // ref: https://spec.modelcontextprotocol.io/specification/basic/messages/
 // ref: https://spec.modelcontextprotocol.io/specification/basic/messages/
 
 
 import { z } from "zod";
 import { z } from "zod";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 
 
 export interface McpRequestMessage {
 export interface McpRequestMessage {
   jsonrpc?: "2.0";
   jsonrpc?: "2.0";
@@ -60,6 +61,32 @@ export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
   params: z.record(z.unknown()).optional(),
   params: z.record(z.unknown()).optional(),
 });
 });
 
 
+////////////
+// Next Chat
+////////////
+export interface ListToolsResponse {
+  tools: {
+    name?: string;
+    description?: string;
+    inputSchema?: object;
+    [key: string]: any;
+  };
+}
+
+export type McpClientData = McpActiveClient | McpErrorClient;
+
+interface McpActiveClient {
+  client: Client;
+  tools: ListToolsResponse;
+  errorMsg: null;
+}
+
+interface McpErrorClient {
+  client: null;
+  tools: null;
+  errorMsg: string;
+}
+
 // MCP 服务器配置相关类型
 // MCP 服务器配置相关类型
 export interface ServerConfig {
 export interface ServerConfig {
   command: string;
   command: string;
@@ -67,23 +94,52 @@ export interface ServerConfig {
   env?: Record<string, string>;
   env?: Record<string, string>;
 }
 }
 
 
-export interface McpConfig {
+export interface McpConfigData {
+  // MCP Server 的配置
   mcpServers: Record<string, ServerConfig>;
   mcpServers: Record<string, ServerConfig>;
 }
 }
 
 
+export const DEFAULT_MCP_CONFIG: McpConfigData = {
+  mcpServers: {},
+};
+
 export interface ArgsMapping {
 export interface ArgsMapping {
+  // 参数映射的类型
   type: "spread" | "single" | "env";
   type: "spread" | "single" | "env";
+
+  // 参数映射的位置
   position?: number;
   position?: number;
+
+  // 参数映射的 key
   key?: string;
   key?: string;
 }
 }
 
 
 export interface PresetServer {
 export interface PresetServer {
+  // MCP Server 的唯一标识,作为最终配置文件 Json 的 key
   id: string;
   id: string;
+
+  // MCP Server 的显示名称
   name: string;
   name: string;
+
+  // MCP Server 的描述
   description: string;
   description: string;
+
+  // MCP Server 的仓库地址
+  repo: string;
+
+  // MCP Server 的标签
+  tags: string[];
+
+  // MCP Server 的命令
   command: string;
   command: string;
+
+  // MCP Server 的参数
   baseArgs: string[];
   baseArgs: string[];
+
+  // MCP Server 是否需要配置
   configurable: boolean;
   configurable: boolean;
+
+  // MCP Server 的配置 schema
   configSchema?: {
   configSchema?: {
     properties: Record<
     properties: Record<
       string,
       string,
@@ -95,5 +151,7 @@ export interface PresetServer {
       }
       }
     >;
     >;
   };
   };
+
+  // MCP Server 的参数映射
   argsMapping?: Record<string, ArgsMapping>;
   argsMapping?: Record<string, ArgsMapping>;
 }
 }

+ 3 - 2
app/page.tsx

@@ -1,12 +1,13 @@
 import { Analytics } from "@vercel/analytics/react";
 import { Analytics } from "@vercel/analytics/react";
 import { Home } from "./components/home";
 import { Home } from "./components/home";
 import { getServerSideConfig } from "./config/server";
 import { getServerSideConfig } from "./config/server";
-import { initializeMcpClients } from "./mcp/actions";
+import { initializeMcpSystem } from "./mcp/actions";
 
 
 const serverConfig = getServerSideConfig();
 const serverConfig = getServerSideConfig();
 
 
 export default async function App() {
 export default async function App() {
-  await initializeMcpClients();
+  // 初始化 MCP 系统
+  await initializeMcpSystem();
 
 
   return (
   return (
     <>
     <>

+ 13 - 12
app/store/chat.ts

@@ -21,8 +21,8 @@ import {
   DEFAULT_SYSTEM_TEMPLATE,
   DEFAULT_SYSTEM_TEMPLATE,
   GEMINI_SUMMARIZE_MODEL,
   GEMINI_SUMMARIZE_MODEL,
   KnowledgeCutOffDate,
   KnowledgeCutOffDate,
-  MCP_PRIMITIVES_TEMPLATE,
   MCP_SYSTEM_TEMPLATE,
   MCP_SYSTEM_TEMPLATE,
+  MCP_TOOLS_TEMPLATE,
   ServiceProvider,
   ServiceProvider,
   StoreKey,
   StoreKey,
   SUMMARIZE_MODEL,
   SUMMARIZE_MODEL,
@@ -35,7 +35,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
 import { useAccessStore } from "./access";
 import { useAccessStore } from "./access";
 import { collectModelsWithDefaultModel } from "../utils/model";
 import { collectModelsWithDefaultModel } from "../utils/model";
 import { createEmptyMask, Mask } from "./mask";
 import { createEmptyMask, Mask } from "./mask";
-import { executeMcpAction, getAllPrimitives } from "../mcp/actions";
+import { executeMcpAction, getAllTools } from "../mcp/actions";
 import { extractMcpJson, isMcpJson } from "../mcp/utils";
 import { extractMcpJson, isMcpJson } from "../mcp/utils";
 
 
 const localStorage = safeLocalStorage();
 const localStorage = safeLocalStorage();
@@ -199,23 +199,24 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
 }
 }
 
 
 async function getMcpSystemPrompt(): Promise<string> {
 async function getMcpSystemPrompt(): Promise<string> {
-  let primitives = await getAllPrimitives();
-  primitives = primitives.filter((i) =>
-    i.primitives.some((p) => p.type === "tool"),
-  );
+  const tools = await getAllTools();
+
+  let toolsStr = "";
+
+  tools.forEach((i) => {
+    // error client has no tools
+    if (!i.tools) return;
 
 
-  let primitivesString = "";
-  primitives.forEach((i) => {
-    primitivesString += MCP_PRIMITIVES_TEMPLATE.replace(
+    toolsStr += MCP_TOOLS_TEMPLATE.replace(
       "{{ clientId }}",
       "{{ clientId }}",
       i.clientId,
       i.clientId,
     ).replace(
     ).replace(
-      "{{ primitives }}",
-      i.primitives.map((p) => JSON.stringify(p, null, 2)).join("\n"),
+      "{{ tools }}",
+      i.tools.tools.map((p: object) => JSON.stringify(p, null, 2)).join("\n"),
     );
     );
   });
   });
 
 
-  return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_PRIMITIVES }}", primitivesString);
+  return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_TOOLS }}", toolsStr);
 }
 }
 
 
 const DEFAULT_CHAT_STATE = {
 const DEFAULT_CHAT_STATE = {

Некоторые файлы не были показаны из-за большого количества измененных файлов