Browse Source

feat: Optimize MCP configuration logic

Kadxy 10 months ago
parent
commit
8aa9a500fd

+ 24 - 0
app/components/chat.tsx

@@ -46,6 +46,7 @@ import QualityIcon from "../icons/hd.svg";
 import StyleIcon from "../icons/palette.svg";
 import PluginIcon from "../icons/plugin.svg";
 import ShortcutkeyIcon from "../icons/shortcutkey.svg";
+import McpToolIcon from "../icons/tool.svg";
 import HeadphoneIcon from "../icons/headphone.svg";
 import {
   BOT_HELLO,
@@ -121,6 +122,7 @@ import { isEmpty } from "lodash-es";
 import { getModelProvider } from "../utils/model";
 import { RealtimeChat } from "@/app/components/realtime-chat";
 import clsx from "clsx";
+import { getAvailableClientsCount } from "../mcp/actions";
 
 const localStorage = safeLocalStorage();
 
@@ -130,6 +132,27 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
   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 }) {
   const chatStore = useChatStore();
   const session = chatStore.currentSession();
@@ -799,6 +822,7 @@ export function ChatActions(props: {
             icon={<ShortcutkeyIcon />}
           />
         )}
+        {!isMobileScreen && <MCPAction />}
       </>
       <div className={styles["chat-input-actions-end"]}>
         {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 { useAccessStore } from "../store";
 import clsx from "clsx";
+import { initializeMcpSystem } from "../mcp/actions";
+import { showToast } from "./ui-lib";
 
 export function Loading(props: { noLogo?: boolean }) {
   return (
@@ -243,6 +245,14 @@ export function Home() {
     useAccessStore.getState().fetch();
   }, []);
 
+  useEffect(() => {
+    // 初始化 MCP 系统
+    initializeMcpSystem().catch((error) => {
+      console.error("Failed to initialize MCP system:", error);
+      showToast("Failed to initialize MCP system");
+    });
+  }, []);
+
   if (!useHasHydrated()) {
     return <Loading />;
   }

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

@@ -39,8 +39,6 @@
     }
 
     .mcp-market-item {
-      display: flex;
-      justify-content: space-between;
       padding: 20px;
       border: var(--border-in-light);
       animation: slide-in ease 0.3s;
@@ -68,118 +66,106 @@
 
       .mcp-market-header {
         display: flex;
-        align-items: center;
+        justify-content: space-between;
+        align-items: flex-start;
+        width: 100%;
 
         .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;
-            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 {
-            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 {
-              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;
           box-shadow: 0 0 0 2px var(--primary-10);
         }
-
-        &::placeholder {
-          color: var(--gray-300) !important;
-          opacity: 1;
-        }
       }
 
       .browse-button {
@@ -534,7 +515,7 @@
     }
   }
 
-  .primitives-list {
+  .tools-list {
     display: flex;
     flex-direction: column;
     gap: 16px;
@@ -545,11 +526,11 @@
     word-break: break-word;
     box-sizing: border-box;
 
-    .primitive-item {
+    .tool-item {
       width: 100%;
       box-sizing: border-box;
 
-      .primitive-name {
+      .tool-name {
         font-size: 14px;
         font-weight: 600;
         color: var(--black);
@@ -560,7 +541,7 @@
         width: 100%;
       }
 
-      .primitive-description {
+      .tool-description {
         font-size: 13px;
         color: var(--gray-500);
         line-height: 1.6;
@@ -590,9 +571,12 @@
       border-radius: 10px;
       padding: 10px;
       margin-bottom: 10px;
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
 
       .list-header {
-        margin-bottom: 10px;
+        margin-bottom: 0;
 
         .list-title {
           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 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 { useState, useEffect } from "react";
+import { useEffect, useState } from "react";
 import presetServersJson from "../mcp/preset-server.json";
-const presetServers = presetServersJson as PresetServer[];
 import {
-  getMcpConfig,
-  updateMcpConfig,
-  getClientPrimitives,
+  addMcpServer,
+  getClientStatus,
+  getClientTools,
+  getMcpConfigFromFile,
+  removeMcpServer,
   restartAllClients,
-  getClientErrors,
-  refreshClientStatus,
 } from "../mcp/actions";
-import { McpConfig, PresetServer, ServerConfig } from "../mcp/types";
+import {
+  ListToolsResponse,
+  McpConfigData,
+  PresetServer,
+  ServerConfig,
+} from "../mcp/types";
 import clsx from "clsx";
 
+const presetServers = presetServersJson as PresetServer[];
+
 interface ConfigProperty {
   type: string;
   description?: string;
@@ -33,67 +40,71 @@ interface ConfigProperty {
 export function McpMarketPage() {
   const navigate = useNavigate();
   const [searchText, setSearchText] = useState("");
-  const [config, setConfig] = useState<McpConfig>({ mcpServers: {} });
+  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 [primitives, setPrimitives] = useState<any[]>([]);
-  const [userConfig, setUserConfig] = useState<Record<string, any>>({});
   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(() => {
-    const init = async () => {
+    const loadInitialState = async () => {
       try {
         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) {
-        showToast("Failed to load configuration");
-        console.error(error);
+        console.error("Failed to load initial state:", error);
+        showToast("Failed to load initial state");
       } finally {
         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(() => {
-    if (editingServerId) {
+    if (editingServerId && config) {
       const currentConfig = config.mcpServers[editingServerId];
       if (currentConfig) {
         // 从当前配置中提取用户配置
@@ -123,7 +134,7 @@ export function McpMarketPage() {
         setUserConfig({});
       }
     }
-  }, [editingServerId, config.mcpServers]);
+  }, [editingServerId, config]);
 
   // 保存服务器配置
   const saveServerConfig = async () => {
@@ -131,6 +142,7 @@ export function McpMarketPage() {
     if (!preset || !preset.configSchema || !editingServerId) return;
 
     try {
+      setIsLoading(true);
       // 构建服务器配置
       const args = [...preset.baseArgs];
       const env: Record<string, string> = {};
@@ -160,22 +172,113 @@ export function McpMarketPage() {
         ...(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);
       showToast("Server configuration saved successfully");
     } catch (error) {
       showToast(
         error instanceof Error ? error.message : "Failed to save configuration",
       );
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  // 获取服务器支持的 Tools
+  const loadTools = async (id: string) => {
+    try {
+      const result = await getClientTools(id);
+      if (result) {
+        setTools(result);
+      } else {
+        throw new Error("Failed to load tools");
+      }
+    } catch (error) {
+      showToast("Failed to load tools");
+      console.error(error);
+      setTools(null);
+    }
+  };
+
+  // 重启所有客户端
+  const handleRestartAll = async () => {
+    try {
+      setIsLoading(true);
+      const newConfig = await restartAllClients();
+      setConfig(newConfig);
+
+      // 更新所有客户端状态
+      const statuses: Record<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]) => {
         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}>
+            <ListItem
+              key={key}
+              title={key}
+              subTitle={prop.description}
+              vertical
+            >
               <div className={styles["path-list"]}>
                 {(currentValue as string[]).map(
                   (value: string, index: number) => (
@@ -197,7 +309,7 @@ export function McpMarketPage() {
                       <input
                         type="text"
                         value={value}
-                        placeholder={`Path ${index + 1}`}
+                        placeholder={`${itemLabel} ${index + 1}`}
                         onChange={(e) => {
                           const newValue = [...currentValue] as string[];
                           newValue[index] = e.target.value;
@@ -218,7 +330,7 @@ export function McpMarketPage() {
                 )}
                 <IconButton
                   icon={<AddIcon />}
-                  text="Add Path"
+                  text={addButtonText}
                   className={styles["add-button"]}
                   bordered
                   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 (
@@ -342,7 +517,7 @@ export function McpMarketPage() {
               )}
             </div>
             <div className="window-header-sub-title">
-              {Object.keys(config.mcpServers).length} servers configured
+              {Object.keys(config?.mcpServers ?? {}).length} servers configured
             </div>
           </div>
 
@@ -351,7 +526,7 @@ export function McpMarketPage() {
               <IconButton
                 icon={<RestartIcon />}
                 bordered
-                onClick={handleRestart}
+                onClick={handleRestartAll}
                 text="Restart All"
                 disabled={isLoading}
               />
@@ -378,121 +553,10 @@ export function McpMarketPage() {
             />
           </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>
 
+        {/*编辑服务器配置*/}
         {editingServerId && (
           <div className="modal-mask">
             <Modal
@@ -521,6 +585,7 @@ export function McpMarketPage() {
           </div>
         )}
 
+        {/*支持的Tools*/}
         {viewingServerId && (
           <div className="modal-mask">
             <Modal
@@ -535,24 +600,20 @@ export function McpMarketPage() {
                 />,
               ]}
             >
-              <div className={styles["primitives-list"]}>
+              <div className={styles["tools-list"]}>
                 {isLoading ? (
                   <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>
-                        {primitive.value.description && (
-                          <div className={styles["primitive-description"]}>
-                            {primitive.value.description}
-                          </div>
-                        )}
                       </div>
-                    ))
+                    ),
+                  )
                 ) : (
                   <div>No tools available</div>
                 )}

+ 6 - 5
app/constant.ts

@@ -88,6 +88,7 @@ export enum StoreKey {
   Update = "chat-update",
   Sync = "sync",
   SdList = "sd-list",
+  Mcp = "mcp-store",
 }
 
 export const DEFAULT_SIDEBAR_WIDTH = 300;
@@ -254,18 +255,18 @@ Latex inline: \\(x^2\\)
 Latex block: $$e=mc^2$$
 `;
 
-export const MCP_PRIMITIVES_TEMPLATE = `
+export const MCP_TOOLS_TEMPLATE = `
 [clientId]
 {{ clientId }}
-[primitives]
-{{ primitives }}
+[tools]
+{{ tools }}
 `;
 
 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.
 
-1. TOOLS AVAILABLE:
-{{ MCP_PRIMITIVES }}
+1. AVAILABLE TOOLS:
+{{ MCP_TOOLS }}
 
 2. WHEN TO USE TOOLS:
    - ALWAYS USE TOOLS when they can help answer user questions

File diff suppressed because it is too large
+ 2 - 0
app/icons/tool.svg


+ 158 - 177
app/mcp/actions.ts

@@ -1,236 +1,217 @@
 "use server";
-import { Client } from "@modelcontextprotocol/sdk/client/index.js";
 import {
   createClient,
   executeRequest,
-  listPrimitives,
-  Primitive,
+  listTools,
+  removeClient,
 } from "./client";
 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 path from "path";
 
 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 {
-    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) {
-    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 {
-    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) {
-    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(
   clientId: string,
   request: McpRequestMessage,
 ) {
   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) {
-    logger.error(`MCP execution error: ${error}`);
+    logger.error(`Failed to execute request for [${clientId}]: ${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 {
-    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) {
-    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 { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 import { MCPClientLogger } from "./logger";
-import { McpRequestMessage } from "./types";
+import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types";
 import { z } from "zod";
 
-export interface ServerConfig {
-  command: string;
-  args?: string[];
-  env?: Record<string, string>;
-}
-
 const logger = new MCPClientLogger();
 
 export async function createClient(
-  serverConfig: ServerConfig,
-  name: string,
+  id: string,
+  config: ServerConfig,
 ): Promise<Client> {
-  logger.info(`Creating client for server ${name}`);
+  logger.info(`Creating client for ${id}...`);
 
   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(
     {
-      name: `nextchat-mcp-client-${name}`,
+      name: `nextchat-mcp-client-${id}`,
       version: "1.0.0",
     },
     {
-      capabilities: {
-        // roots: {
-        //   listChanged: true,
-        // },
-      },
+      capabilities: {},
     },
   );
   await client.connect(transport);
   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(
   client: Client,
   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 conf from "./mcp_config.json";
 
 const logger = new MCPClientLogger("MCP Server Example", true);
 
-const TEST_SERVER = "everything";
+const TEST_SERVER = "filesystem";
 
 async function main() {
   logger.info(`All MCP servers: ${Object.keys(conf.mcpServers).join(", ")}`);
 
   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.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",
     "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",
     "baseArgs": ["-y", "@modelcontextprotocol/server-filesystem"],
     "configurable": true,
@@ -12,7 +14,9 @@
           "type": "array",
           "description": "Allowed file system paths",
           "required": true,
-          "minItems": 1
+          "minItems": 1,
+          "itemLabel": "Path",
+          "addButtonText": "Add Path"
         }
       }
     },
@@ -27,6 +31,8 @@
     "id": "github",
     "name": "GitHub",
     "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",
     "baseArgs": ["-y", "@modelcontextprotocol/server-github"],
     "configurable": true,
@@ -50,6 +56,8 @@
     "id": "gdrive",
     "name": "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",
     "baseArgs": ["-y", "@modelcontextprotocol/server-gdrive"],
     "configurable": false
@@ -58,6 +66,8 @@
     "id": "playwright",
     "name": "Playwright",
     "description": "Browser automation and webscrapping with Playwright",
+    "repo": "https://github.com/executeautomation/mcp-playwright",
+    "tags": ["browser", "automation", "scraping"],
     "command": "npx",
     "baseArgs": ["-y", "@executeautomation/playwright-mcp-server"],
     "configurable": false
@@ -66,6 +76,8 @@
     "id": "mongodb",
     "name": "MongoDB",
     "description": "Direct interaction with MongoDB databases",
+    "repo": "",
+    "tags": ["database", "mongodb", "nosql"],
     "command": "node",
     "baseArgs": ["dist/index.js"],
     "configurable": true,
@@ -89,6 +101,8 @@
     "id": "difyworkflow",
     "name": "Dify Workflow",
     "description": "Tools to query and execute Dify workflows",
+    "repo": "https://github.com/gotoolkits/mcp-difyworkflow-server",
+    "tags": ["workflow", "automation", "dify"],
     "command": "mcp-difyworkflow-server",
     "baseArgs": ["-base-url"],
     "configurable": true,
@@ -130,6 +144,8 @@
     "id": "postgres",
     "name": "PostgreSQL",
     "description": "Read-only database access with schema inspection",
+    "repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres",
+    "tags": ["database", "postgresql", "sql"],
     "command": "docker",
     "baseArgs": ["run", "-i", "--rm", "mcp/postgres"],
     "configurable": true,
@@ -153,6 +169,8 @@
     "id": "brave-search",
     "name": "Brave Search",
     "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",
     "baseArgs": ["-y", "@modelcontextprotocol/server-brave-search"],
     "configurable": true,
@@ -176,6 +194,8 @@
     "id": "google-maps",
     "name": "Google Maps",
     "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",
     "baseArgs": ["-y", "@modelcontextprotocol/server-google-maps"],
     "configurable": true,
@@ -199,6 +219,8 @@
     "id": "docker-mcp",
     "name": "Docker",
     "description": "Run and manage docker containers, docker compose, and logs",
+    "repo": "https://github.com/QuantGeekDev/docker-mcp",
+    "tags": ["docker", "container", "devops"],
     "command": "uvx",
     "baseArgs": ["docker-mcp"],
     "configurable": false

+ 59 - 1
app/mcp/types.ts

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

+ 3 - 2
app/page.tsx

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

+ 13 - 12
app/store/chat.ts

@@ -21,8 +21,8 @@ import {
   DEFAULT_SYSTEM_TEMPLATE,
   GEMINI_SUMMARIZE_MODEL,
   KnowledgeCutOffDate,
-  MCP_PRIMITIVES_TEMPLATE,
   MCP_SYSTEM_TEMPLATE,
+  MCP_TOOLS_TEMPLATE,
   ServiceProvider,
   StoreKey,
   SUMMARIZE_MODEL,
@@ -35,7 +35,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
 import { useAccessStore } from "./access";
 import { collectModelsWithDefaultModel } from "../utils/model";
 import { createEmptyMask, Mask } from "./mask";
-import { executeMcpAction, getAllPrimitives } from "../mcp/actions";
+import { executeMcpAction, getAllTools } from "../mcp/actions";
 import { extractMcpJson, isMcpJson } from "../mcp/utils";
 
 const localStorage = safeLocalStorage();
@@ -199,23 +199,24 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
 }
 
 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 }}",
       i.clientId,
     ).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 = {

Some files were not shown because too many files changed in this diff