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

This commit is contained in:
Kadxy 2025-01-18 21:19:01 +08:00
parent 0112b54bc7
commit bc71ae247b
10 changed files with 161 additions and 87 deletions

View File

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

View File

@ -34,12 +34,16 @@ ENV PROXY_URL=""
ENV OPENAI_API_KEY="" ENV OPENAI_API_KEY=""
ENV GOOGLE_API_KEY="" ENV GOOGLE_API_KEY=""
ENV CODE="" ENV CODE=""
ENV ENABLE_MCP=""
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/server ./.next/server 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 EXPOSE 3000
CMD if [ -n "$PROXY_URL" ]; then \ CMD if [ -n "$PROXY_URL" ]; then \

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,10 @@
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 { initializeMcpSystem } from "./mcp/actions";
const serverConfig = getServerSideConfig(); const serverConfig = getServerSideConfig();
export default async function App() { export default async function App() {
// 初始化 MCP 系统
await initializeMcpSystem();
return ( return (
<> <>
<Home /> <Home />