diff --git a/.env.template b/.env.template index 82f44216ab8..c0cd80c65df 100644 --- a/.env.template +++ b/.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. diff --git a/Dockerfile b/Dockerfile index ae9a17cddbd..ff009b17848 100644 --- a/Dockerfile +++ b/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 \ diff --git a/app/components/chat.tsx b/app/components/chat.tsx index c8d6886e562..435a13b76bb 100644 --- a/app/components/chat.tsx +++ b/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(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 ( navigate(Path.McpMarket)} diff --git a/app/components/home.tsx b/app/components/home.tsx index 8a03c50b6dc..98f759a4803 100644 --- a/app/components/home.tsx +++ b/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()) { diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index a7cea879de7..98211ceddbf 100644 --- a/app/components/mcp-market.tsx +++ b/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>({}); const [editingServerId, setEditingServerId] = useState(); @@ -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 = {}; - 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 = {}; + 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 = { - 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() { } text="Start" - onClick={() => resumeServer(server.id)} + onClick={() => restartServer(server.id)} disabled={isLoading} /> {/* )} - {/*支持的Tools*/} {viewingServerId && (
(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 ( - } - text={shouldNarrow ? undefined : Locale.Mcp.Name} - className={styles["sidebar-bar-button"]} - onClick={() => { - navigate(Path.McpMarket, { state: { fromHome: true } }); - }} - shadow - /> + {mcpEnabled && ( + } + text={shouldNarrow ? undefined : Locale.Mcp.Name} + className={styles["sidebar-bar-button"]} + onClick={() => { + navigate(Path.McpMarket, { state: { fromHome: true } }); + }} + shadow + /> + )} } text={shouldNarrow ? undefined : Locale.Discovery.Name} diff --git a/app/config/server.ts b/app/config/server.ts index 9d6b3c2b8da..ab7a775c2dc 100644 --- a/app/config/server.ts +++ b/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, }; }; diff --git a/app/layout.tsx b/app/layout.tsx index 7d14cb88d70..47c058fb300 100644 --- a/app/layout.tsx +++ b/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 ( diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index 2248d1327c6..7d4b5b661dd 100644 --- a/app/mcp/actions.ts +++ b/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; + } +} diff --git a/app/page.tsx b/app/page.tsx index 48a70220190..c748d42c71a 100644 --- a/app/page.tsx +++ b/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 ( <>