Browse Source

feat: add ENABLE_MCP env var to toggle MCP feature globally and in Docker

Kadxy 10 tháng trước cách đây
mục cha
commit
bc71ae247b

+ 5 - 0
.env.template

@@ -7,6 +7,11 @@ CODE=your-password
 # You can start service behind a proxy. (optional)
 PROXY_URL=http://localhost:7890
 
+# Enable MCP functionality (optional)
+# Default: Empty (disabled)
+# Set to "true" to enable MCP functionality
+ENABLE_MCP=
+
 # (optional)
 # Default: Empty
 # Google Gemini Pro API key, set if you want to use Google Gemini Pro API.

+ 4 - 0
Dockerfile

@@ -34,12 +34,16 @@ ENV PROXY_URL=""
 ENV OPENAI_API_KEY=""
 ENV GOOGLE_API_KEY=""
 ENV CODE=""
+ENV ENABLE_MCP=""
 
 COPY --from=builder /app/public ./public
 COPY --from=builder /app/.next/standalone ./
 COPY --from=builder /app/.next/static ./.next/static
 COPY --from=builder /app/.next/server ./.next/server
 
+RUN mkdir -p /app/app/mcp && chmod 777 /app/app/mcp
+COPY --from=builder /app/app/mcp/mcp_config.json /app/app/mcp/
+
 EXPOSE 3000
 
 CMD if [ -n "$PROXY_URL" ]; then \

+ 12 - 5
app/components/chat.tsx

@@ -122,7 +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";
+import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions";
 
 const localStorage = safeLocalStorage();
 
@@ -135,15 +135,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
 const MCPAction = () => {
   const navigate = useNavigate();
   const [count, setCount] = useState<number>(0);
+  const [mcpEnabled, setMcpEnabled] = useState(false);
 
   useEffect(() => {
-    const loadCount = async () => {
-      const count = await getAvailableClientsCount();
-      setCount(count);
+    const checkMcpStatus = async () => {
+      const enabled = await isMcpEnabled();
+      setMcpEnabled(enabled);
+      if (enabled) {
+        const count = await getAvailableClientsCount();
+        setCount(count);
+      }
     };
-    loadCount();
+    checkMcpStatus();
   }, []);
 
+  if (!mcpEnabled) return null;
+
   return (
     <ChatAction
       onClick={() => navigate(Path.McpMarket)}

+ 14 - 9
app/components/home.tsx

@@ -29,8 +29,7 @@ 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";
+import { initializeMcpSystem, isMcpEnabled } from "../mcp/actions";
 
 export function Loading(props: { noLogo?: boolean }) {
   return (
@@ -243,14 +242,20 @@ export function Home() {
   useEffect(() => {
     console.log("[Config] got config from build time", getClientConfig());
     useAccessStore.getState().fetch();
-  }, []);
 
-  useEffect(() => {
-    // 初始化 MCP 系统
-    initializeMcpSystem().catch((error) => {
-      console.error("Failed to initialize MCP system:", error);
-      showToast("Failed to initialize MCP system");
-    });
+    const initMcp = async () => {
+      try {
+        const enabled = await isMcpEnabled();
+        if (enabled) {
+          console.log("[MCP] initializing...");
+          await initializeMcpSystem();
+          console.log("[MCP] initialized");
+        }
+      } catch (err) {
+        console.error("[MCP] failed to initialize:", err);
+      }
+    };
+    initMcp();
   }, []);
 
   if (!useHasHydrated()) {

+ 73 - 55
app/components/mcp-market.tsx

@@ -16,8 +16,9 @@ import {
   getClientStatus,
   getClientTools,
   getMcpConfigFromFile,
-  restartAllClients,
+  isMcpEnabled,
   pauseMcpServer,
+  restartAllClients,
   resumeMcpServer,
 } from "../mcp/actions";
 import {
@@ -30,6 +31,7 @@ import {
 import clsx from "clsx";
 import PlayIcon from "../icons/play.svg";
 import StopIcon from "../icons/pause.svg";
+import { Path } from "../constant";
 
 interface ConfigProperty {
   type: string;
@@ -40,6 +42,7 @@ interface ConfigProperty {
 
 export function McpMarketPage() {
   const navigate = useNavigate();
+  const [mcpEnabled, setMcpEnabled] = useState(false);
   const [searchText, setSearchText] = useState("");
   const [userConfig, setUserConfig] = useState<Record<string, any>>({});
   const [editingServerId, setEditingServerId] = useState<string | undefined>();
@@ -56,8 +59,22 @@ export function McpMarketPage() {
     {},
   );
 
+  // 检查 MCP 是否启用
+  useEffect(() => {
+    const checkMcpStatus = async () => {
+      const enabled = await isMcpEnabled();
+      setMcpEnabled(enabled);
+      if (!enabled) {
+        navigate(Path.Home);
+      }
+    };
+    checkMcpStatus();
+  }, [navigate]);
+
+  // 加载预设服务器
   useEffect(() => {
     const loadPresetServers = async () => {
+      if (!mcpEnabled) return;
       try {
         setLoadingPresets(true);
         const response = await fetch("https://nextchat.club/mcp/list");
@@ -73,17 +90,13 @@ export function McpMarketPage() {
         setLoadingPresets(false);
       }
     };
-    loadPresetServers().then();
-  }, []);
-
-  // 检查服务器是否已添加
-  const isServerAdded = (id: string) => {
-    return id in (config?.mcpServers ?? {});
-  };
+    loadPresetServers();
+  }, [mcpEnabled]);
 
-  // 从服务器获取初始状态
+  // 加载初始状态
   useEffect(() => {
     const loadInitialState = async () => {
+      if (!mcpEnabled) return;
       try {
         setIsLoading(true);
         const config = await getMcpConfigFromFile();
@@ -103,42 +116,50 @@ export function McpMarketPage() {
       }
     };
     loadInitialState();
-  }, []);
+  }, [mcpEnabled]);
 
   // 加载当前编辑服务器的配置
   useEffect(() => {
-    if (editingServerId && config) {
-      const currentConfig = config.mcpServers[editingServerId];
-      if (currentConfig) {
-        // 从当前配置中提取用户配置
-        const preset = presetServers.find((s) => s.id === editingServerId);
-        if (preset?.configSchema) {
-          const userConfig: Record<string, any> = {};
-          Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
-            if (mapping.type === "spread") {
-              // 对于 spread 类型,从 args 中提取数组
-              const startPos = mapping.position ?? 0;
-              userConfig[key] = currentConfig.args.slice(startPos);
-            } else if (mapping.type === "single") {
-              // 对于 single 类型,获取单个值
-              userConfig[key] = currentConfig.args[mapping.position ?? 0];
-            } else if (
-              mapping.type === "env" &&
-              mapping.key &&
-              currentConfig.env
-            ) {
-              // 对于 env 类型,从环境变量中获取值
-              userConfig[key] = currentConfig.env[mapping.key];
-            }
-          });
-          setUserConfig(userConfig);
-        }
-      } else {
-        setUserConfig({});
+    if (!editingServerId || !config) return;
+    const currentConfig = config.mcpServers[editingServerId];
+    if (currentConfig) {
+      // 从当前配置中提取用户配置
+      const preset = presetServers.find((s) => s.id === editingServerId);
+      if (preset?.configSchema) {
+        const userConfig: Record<string, any> = {};
+        Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
+          if (mapping.type === "spread") {
+            // For spread types, extract the array from args.
+            const startPos = mapping.position ?? 0;
+            userConfig[key] = currentConfig.args.slice(startPos);
+          } else if (mapping.type === "single") {
+            // For single types, get a single value
+            userConfig[key] = currentConfig.args[mapping.position ?? 0];
+          } else if (
+            mapping.type === "env" &&
+            mapping.key &&
+            currentConfig.env
+          ) {
+            // For env types, get values from environment variables
+            userConfig[key] = currentConfig.env[mapping.key];
+          }
+        });
+        setUserConfig(userConfig);
       }
+    } else {
+      setUserConfig({});
     }
   }, [editingServerId, config, presetServers]);
 
+  if (!mcpEnabled) {
+    return null;
+  }
+
+  // 检查服务器是否已添加
+  const isServerAdded = (id: string) => {
+    return id in (config?.mcpServers ?? {});
+  };
+
   // 保存服务器配置
   const saveServerConfig = async () => {
     const preset = presetServers.find((s) => s.id === editingServerId);
@@ -291,8 +312,8 @@ export function McpMarketPage() {
     }
   };
 
-  // 修改恢复服务器函数
-  const resumeServer = async (id: string) => {
+  // Restart server
+  const restartServer = async (id: string) => {
     try {
       updateLoadingState(id, "Starting server...");
 
@@ -320,7 +341,7 @@ export function McpMarketPage() {
     }
   };
 
-  // 修改重启所有客户端函数
+  // Restart all clients
   const handleRestartAll = async () => {
     try {
       updateLoadingState("all", "Restarting all servers...");
@@ -342,7 +363,7 @@ export function McpMarketPage() {
     }
   };
 
-  // 渲染配置表单
+  // Render configuration form
   const renderConfigForm = () => {
     const preset = presetServers.find((s) => s.id === editingServerId);
     if (!preset?.configSchema) return null;
@@ -422,12 +443,10 @@ export function McpMarketPage() {
     );
   };
 
-  // 检查服务器状态
   const checkServerStatus = (clientId: string) => {
     return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
   };
 
-  // 修改状态显示逻辑
   const getServerStatusDisplay = (clientId: string) => {
     const status = checkServerStatus(clientId);
 
@@ -450,7 +469,7 @@ export function McpMarketPage() {
     return statusMap[status.status];
   };
 
-  // 获取操作状态的类型
+  // Get the type of operation status
   const getOperationStatusType = (message: string) => {
     if (message.toLowerCase().includes("stopping")) return "stopping";
     if (message.toLowerCase().includes("starting")) return "starting";
@@ -496,15 +515,15 @@ export function McpMarketPage() {
 
         // 定义状态优先级
         const statusPriority: Record<string, number> = {
-          error: 0, // 错误状态最高优先级
-          active: 1, // 已启动次之
-          starting: 2, // 正在启动
-          stopping: 3, // 正在停止
-          paused: 4, // 已暂停
-          undefined: 5, // 未配置最低优先级
+          error: 0, // Highest priority for error status
+          active: 1, // Second for active
+          starting: 2, // Starting
+          stopping: 3, // Stopping
+          paused: 4, // Paused
+          undefined: 5, // Lowest priority for undefined
         };
 
-        // 获取实际状态(包括加载状态)
+        // Get actual status (including loading status)
         const getEffectiveStatus = (status: string, loading?: string) => {
           if (loading) {
             const operationType = getOperationStatusType(loading);
@@ -524,7 +543,7 @@ export function McpMarketPage() {
           );
         }
 
-        // 状态相同时按名称排序
+        // Sort by name when statuses are the same
         return a.name.localeCompare(b.name);
       })
       .map((server) => (
@@ -591,7 +610,7 @@ export function McpMarketPage() {
                       <IconButton
                         icon={<PlayIcon />}
                         text="Start"
-                        onClick={() => resumeServer(server.id)}
+                        onClick={() => restartServer(server.id)}
                         disabled={isLoading}
                       />
                       {/* <IconButton
@@ -720,7 +739,6 @@ export function McpMarketPage() {
           </div>
         )}
 
-        {/*支持的Tools*/}
         {viewingServerId && (
           <div className="modal-mask">
             <Modal

+ 26 - 11
app/components/sidebar.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
+import React, { Fragment, useEffect, useMemo, useRef, useState } from "react";
 
 import styles from "./home.module.scss";
 
@@ -30,8 +30,9 @@ import {
 import { Link, useNavigate } from "react-router-dom";
 import { isIOS, useMobileScreen } from "../utils";
 import dynamic from "next/dynamic";
-import { showConfirm, Selector } from "./ui-lib";
+import { Selector, showConfirm } from "./ui-lib";
 import clsx from "clsx";
+import { isMcpEnabled } from "../mcp/actions";
 
 const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
   loading: () => null,
@@ -129,6 +130,7 @@ export function useDragSideBar() {
     shouldNarrow,
   };
 }
+
 export function SideBarContainer(props: {
   children: React.ReactNode;
   onDragStart: (e: MouseEvent) => void;
@@ -224,6 +226,17 @@ export function SideBar(props: { className?: string }) {
   const navigate = useNavigate();
   const config = useAppConfig();
   const chatStore = useChatStore();
+  const [mcpEnabled, setMcpEnabled] = useState(false);
+
+  useEffect(() => {
+    // 检查 MCP 是否启用
+    const checkMcpStatus = async () => {
+      const enabled = await isMcpEnabled();
+      setMcpEnabled(enabled);
+      console.log("[SideBar] MCP enabled:", enabled);
+    };
+    checkMcpStatus();
+  }, []);
 
   return (
     <SideBarContainer
@@ -251,15 +264,17 @@ export function SideBar(props: { className?: string }) {
             }}
             shadow
           />
-          <IconButton
-            icon={<McpIcon />}
-            text={shouldNarrow ? undefined : Locale.Mcp.Name}
-            className={styles["sidebar-bar-button"]}
-            onClick={() => {
-              navigate(Path.McpMarket, { state: { fromHome: true } });
-            }}
-            shadow
-          />
+          {mcpEnabled && (
+            <IconButton
+              icon={<McpIcon />}
+              text={shouldNarrow ? undefined : Locale.Mcp.Name}
+              className={styles["sidebar-bar-button"]}
+              onClick={() => {
+                navigate(Path.McpMarket, { state: { fromHome: true } });
+              }}
+              shadow
+            />
+          )}
           <IconButton
             icon={<DiscoveryIcon />}
             text={shouldNarrow ? undefined : Locale.Discovery.Name}

+ 6 - 1
app/config/server.ts

@@ -81,6 +81,8 @@ declare global {
 
       // custom template for preprocessing user input
       DEFAULT_INPUT_TEMPLATE?: string;
+
+      ENABLE_MCP?: string; // enable mcp functionality
     }
   }
 }
@@ -129,7 +131,9 @@ export const getServerSideConfig = () => {
     if (customModels) customModels += ",";
     customModels += DEFAULT_MODELS.filter(
       (m) =>
-        (m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o") || m.name.startsWith("o1")) &&
+        (m.name.startsWith("gpt-4") ||
+          m.name.startsWith("chatgpt-4o") ||
+          m.name.startsWith("o1")) &&
         !m.name.startsWith("gpt-4o-mini"),
     )
       .map((m) => "-" + m.name)
@@ -249,5 +253,6 @@ export const getServerSideConfig = () => {
     customModels,
     defaultModel,
     allowedWebDavEndpoints,
+    enableMcp: !!process.env.ENABLE_MCP,
   };
 };

+ 3 - 2
app/layout.tsx

@@ -5,9 +5,8 @@ import "./styles/highlight.scss";
 import { getClientConfig } from "./config/client";
 import type { Metadata, Viewport } from "next";
 import { SpeedInsights } from "@vercel/speed-insights/next";
-import { getServerSideConfig } from "./config/server";
 import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google";
-const serverConfig = getServerSideConfig();
+import { getServerSideConfig } from "./config/server";
 
 export const metadata: Metadata = {
   title: "NextChat",
@@ -33,6 +32,8 @@ export default function RootLayout({
 }: {
   children: React.ReactNode;
 }) {
+  const serverConfig = getServerSideConfig();
+
   return (
     <html lang="en">
       <head>

+ 18 - 0
app/mcp/actions.ts

@@ -16,6 +16,7 @@ import {
 } from "./types";
 import fs from "fs/promises";
 import path from "path";
+import { getServerSideConfig } from "../config/server";
 
 const logger = new MCPClientLogger("MCP Actions");
 const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
@@ -117,6 +118,12 @@ async function initializeSingleClient(
 export async function initializeMcpSystem() {
   logger.info("MCP Actions starting...");
   try {
+    // 检查是否已有活跃的客户端
+    if (clientsMap.size > 0) {
+      logger.info("MCP system already initialized, skipping...");
+      return;
+    }
+
     const config = await getMcpConfigFromFile();
     // 初始化所有客户端
     for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
@@ -352,3 +359,14 @@ export async function reinitializeClient(clientId: string) {
   }
   await initializeSingleClient(clientId, serverConfig);
 }
+
+// 检查 MCP 是否启用
+export async function isMcpEnabled() {
+  try {
+    const serverConfig = getServerSideConfig();
+    return !!serverConfig.enableMcp;
+  } catch (error) {
+    logger.error(`Failed to check MCP status: ${error}`);
+    return false;
+  }
+}

+ 0 - 4
app/page.tsx

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