diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index 235f63b1c..485ffa284 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -22,11 +22,12 @@ import { resumeMcpServer, } from "../mcp/actions"; import { - ListToolsResponse, + ToolSchema, McpConfigData, PresetServer, ServerConfig, ServerStatusResponse, + isServerStdioConfig, } from "../mcp/types"; import clsx from "clsx"; import PlayIcon from "../icons/play.svg"; @@ -46,7 +47,7 @@ export function McpMarketPage() { const [searchText, setSearchText] = useState(""); const [userConfig, setUserConfig] = useState>({}); const [editingServerId, setEditingServerId] = useState(); - const [tools, setTools] = useState(null); + const [tools, setTools] = useState(null); const [viewingServerId, setViewingServerId] = useState(); const [isLoading, setIsLoading] = useState(false); const [config, setConfig] = useState(); @@ -136,7 +137,7 @@ export function McpMarketPage() { useEffect(() => { if (!editingServerId || !config) return; const currentConfig = config.mcpServers[editingServerId]; - if (currentConfig) { + if (isServerStdioConfig(currentConfig)) { // 从当前配置中提取用户配置 const preset = presetServers.find((s) => s.id === editingServerId); if (preset?.configSchema) { @@ -230,7 +231,7 @@ export function McpMarketPage() { try { const result = await getClientTools(id); if (result) { - setTools(result); + setTools(result?.tools); } else { throw new Error("Failed to load tools"); } @@ -731,17 +732,15 @@ export function McpMarketPage() {
{isLoading ? (
Loading...
- ) : tools?.tools ? ( - tools.tools.map( - (tool: ListToolsResponse["tools"], index: number) => ( -
-
{tool.name}
-
- {tool.description} -
+ ) : tools ? ( + tools.map((tool: ToolSchema, index: number) => ( +
+
{tool.name}
+
+ {tool.description}
- ), - ) +
+ )) ) : (
No tools available
)} diff --git a/app/config/build.ts b/app/config/build.ts index b2b1ad49d..f83bbf37c 100644 --- a/app/config/build.ts +++ b/app/config/build.ts @@ -40,6 +40,17 @@ export const getBuildConfig = () => { buildMode, isApp, template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE, + + needCode: !!process.env.CODE, + hideUserApiKey: !!process.env.HIDE_USER_API_KEY, + baseUrl: process.env.BASE_URL, + openaiUrl: process.env.OPENAI_BASE_URL ?? process.env.BASE_URL, + disableGPT4: !!process.env.DISABLE_GPT4, + useCustomConfig: !!process.env.USE_CUSTOM_CONFIG, + hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, + disableFastLink: !!process.env.DISABLE_FAST_LINK, + defaultModel: process.env.DEFAULT_MODEL ?? "", + enableMcp: process.env.ENABLE_MCP === "true", }; }; diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index e8b1ad1d0..e330d2e93 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -1,4 +1,6 @@ -"use server"; +if (!EXPORT_MODE) { + ("use server"); +} import { createClient, executeRequest, @@ -14,14 +16,22 @@ import { ServerConfig, ServerStatusResponse, } from "./types"; -import fs from "fs/promises"; -import path from "path"; -import { getServerSideConfig } from "../config/server"; + +const JSON_INDENT = 2; const logger = new MCPClientLogger("MCP Actions"); -const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json"); + +const getConfigPath = async () => { + if (EXPORT_MODE) { + return "/mcp/config.json"; + } else { + const path = await import("path"); + return path.join(process.cwd(), "app/mcp/mcp_config.json"); + } +}; const clientsMap = new Map(); +const toolToClientMap = new Map(); // 获取客户端状态 export async function getClientsStatus(): Promise< @@ -126,6 +136,13 @@ async function initializeSingleClient( `Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`, ); clientsMap.set(clientId, { client, tools, errorMsg: null }); + if (tools?.tools) { + for (const tool of tools.tools) { + if (tool.name) { + toolToClientMap.set(tool.name, clientId); + } + } + } logger.success(`Client [${clientId}] initialized successfully`); }) .catch((error) => { @@ -243,6 +260,13 @@ export async function resumeMcpServer(clientId: string): Promise { const client = await createClient(clientId, serverConfig); const tools = await listTools(client); clientsMap.set(clientId, { client, tools, errorMsg: null }); + if (tools?.tools) { + for (const tool of tools.tools) { + if (tool.name) { + toolToClientMap.set(tool.name, clientId); + } + } + } logger.success(`Client [${clientId}] initialized successfully`); // 初始化成功后更新配置 @@ -339,7 +363,19 @@ export async function executeMcpAction( request: McpRequestMessage, ) { try { - const client = clientsMap.get(clientId); + let client = clientsMap.get(clientId); + if ( + !client && + request.params?.name && + typeof request.params.name === "string" + ) { + // Use a tool-to-client mapping that's maintained when tools are initialized + const toolName = request.params.name; + const toolClientId = toolToClientMap.get(toolName); + if (toolClientId) { + client = clientsMap.get(toolClientId); + } + } if (!client?.client) { throw new Error(`Client ${clientId} not found`); } @@ -354,8 +390,30 @@ export async function executeMcpAction( // 获取 MCP 配置文件 export async function getMcpConfigFromFile(): Promise { try { - const configStr = await fs.readFile(CONFIG_PATH, "utf-8"); - return JSON.parse(configStr); + if (EXPORT_MODE) { + const res = await fetch(await getConfigPath()); + const config: McpConfigData = await res.json(); + const storage = localStorage; + const storedConfig_str = storage.getItem("McpConfig"); + if (storedConfig_str) { + const storedConfig: McpConfigData = JSON.parse(storedConfig_str); + // Create a merged configuration that combines both sources + const merged = { ...config.mcpServers }; + if (storedConfig.mcpServers) { + // Ensure we process all servers from stored config + for (const id in storedConfig.mcpServers) { + merged[id] = { ...merged[id], ...storedConfig.mcpServers[id] }; + } + } + + config.mcpServers = merged; + } + return config; + } else { + const fs = await import("fs/promises"); + const configStr = await fs.readFile(await getConfigPath(), "utf-8"); + return JSON.parse(configStr); + } } catch (error) { logger.error(`Failed to load MCP config, using default config: ${error}`); return DEFAULT_MCP_CONFIG; @@ -364,20 +422,42 @@ export async function getMcpConfigFromFile(): Promise { // 更新 MCP 配置文件 async function updateMcpConfig(config: McpConfigData): Promise { - try { + if (EXPORT_MODE) { + try { + const storage = localStorage; + storage.setItem("McpConfig", JSON.stringify(config)); + } catch (storageError) { + logger.warn(`Failed to save MCP config to localStorage: ${storageError}`); + // Continue execution without storage + } + } else { + const fs = await import("fs/promises"); + const path = await import("path"); // 确保目录存在 - await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true }); - await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2)); - } catch (error) { - throw error; + await fs.mkdir(path.dirname(await getConfigPath()), { recursive: true }); + await fs.writeFile( + await getConfigPath(), + JSON.stringify(config, null, JSON_INDENT), + ); } } // 检查 MCP 是否启用 export async function isMcpEnabled() { try { - const serverConfig = getServerSideConfig(); - return serverConfig.enableMcp; + const config = await getMcpConfigFromFile(); + if (typeof config.enableMcp === "boolean") { + return config.enableMcp; + } + if (EXPORT_MODE) { + const { getClientConfig } = await import("../config/client"); + const clientConfig = getClientConfig(); + return clientConfig?.enableMcp === true; + } else { + const { getServerSideConfig } = await import("../config/server"); + const serverConfig = getServerSideConfig(); + return serverConfig.enableMcp; + } } catch (error) { logger.error(`Failed to check MCP status: ${error}`); return false; diff --git a/app/mcp/client.ts b/app/mcp/client.ts index 5c2f071e3..3154b49f3 100644 --- a/app/mcp/client.ts +++ b/app/mcp/client.ts @@ -1,7 +1,11 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { MCPClientLogger } from "./logger"; -import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types"; +import { + ListToolsResponse, + McpRequestMessage, + ServerConfig, + isServerSseConfig, +} from "./types"; import { z } from "zod"; const logger = new MCPClientLogger(); @@ -12,18 +16,36 @@ export async function createClient( ): Promise { logger.info(`Creating client for ${id}...`); - const transport = new StdioClientTransport({ - command: config.command, - args: config.args, - env: { - ...Object.fromEntries( - Object.entries(process.env) - .filter(([_, v]) => v !== undefined) - .map(([k, v]) => [k, v as string]), - ), - ...(config.env || {}), - }, - }); + let transport; + + if (isServerSseConfig(config)) { + const { SSEClientTransport } = await import( + "@modelcontextprotocol/sdk/client/sse.js" + ); + transport = new SSEClientTransport(new URL(config.url)); + } else { + if (EXPORT_MODE) { + throw new Error( + "Cannot use stdio transport in export mode. Please use SSE transport configuration instead.", + ); + } else { + const { StdioClientTransport } = await import( + "@modelcontextprotocol/sdk/client/stdio.js" + ); + transport = new StdioClientTransport({ + command: config.command, + args: config.args, + env: { + ...Object.fromEntries( + Object.entries(process.env) + .filter(([_, v]) => v !== undefined) + .map(([k, v]) => [k, v as string]), + ), + ...(config.env || {}), + }, + }); + } + } const client = new Client( { diff --git a/app/mcp/types.ts b/app/mcp/types.ts index 45d1d979a..ea63bb64a 100644 --- a/app/mcp/types.ts +++ b/app/mcp/types.ts @@ -8,6 +8,7 @@ export interface McpRequestMessage { id?: string | number; method: "tools/call" | string; params?: { + name?: string; [key: string]: unknown; }; } @@ -65,12 +66,14 @@ export const McpNotificationsSchema: z.ZodType = z.object({ // Next Chat //////////// export interface ListToolsResponse { - tools: { - name?: string; - description?: string; - inputSchema?: object; - [key: string]: any; - }; + tools: ToolSchema[]; +} + +export interface ToolSchema { + name?: string; + description?: string; + inputSchema?: object; + [key: string]: any; } export type McpClientData = @@ -110,14 +113,31 @@ export interface ServerStatusResponse { } // MCP 服务器配置相关类型 -export interface ServerConfig { + +export const isServerSseConfig = (c?: ServerConfig): c is ServerSseConfig => + c !== null && typeof c === "object" && c.type === "sse"; +export const isServerStdioConfig = (c?: ServerConfig): c is ServerStdioConfig => + c !== null && typeof c === "object" && (!c.type || c.type === "stdio"); + +export type ServerConfig = ServerStdioConfig | ServerSseConfig; + +export interface ServerStdioConfig { + type?: "stdio"; command: string; args: string[]; env?: Record; status?: "active" | "paused" | "error"; } +export interface ServerSseConfig { + type: "sse"; + url: string; + headers?: Record; + status?: "active" | "paused" | "error"; +} + export interface McpConfigData { + enableMcp?: boolean; // MCP Server 的配置 mcpServers: Record; } diff --git a/app/store/access.ts b/app/store/access.ts index 7025a1814..524efb515 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -243,7 +243,12 @@ export const useAccessStore = createPersistStore( ); }, fetch() { - if (fetchState > 0 || getClientConfig()?.buildMode === "export") return; + const clientConfig = getClientConfig(); + if (!(fetchState > 0) && clientConfig?.buildMode === "export") { + set(clientConfig); + fetchState = 2; + } + if (fetchState > 0 || clientConfig?.buildMode === "export") return; fetchState = 1; fetch("/api/config", { method: "post", diff --git a/app/typing.ts b/app/typing.ts index ecb327936..cac15e1b3 100644 --- a/app/typing.ts +++ b/app/typing.ts @@ -1,3 +1,7 @@ +declare global { + const EXPORT_MODE: boolean; +} + export type Updater = (updater: (value: T) => void) => void; export const ROLES = ["system", "user", "assistant"] as const; diff --git a/next.config.mjs b/next.config.mjs index 0e1105d56..afcd1fb75 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -6,9 +6,15 @@ console.log("[Next] build mode", mode); const disableChunk = !!process.env.DISABLE_CHUNK || mode === "export"; console.log("[Next] build with chunk: ", !disableChunk); +const EXPORT_MODE = mode === "export"; + + /** @type {import('next').NextConfig} */ const nextConfig = { webpack(config) { + config.plugins.push(new webpack.DefinePlugin({ + EXPORT_MODE: EXPORT_MODE + })); config.module.rules.push({ test: /\.svg$/, use: ["@svgr/webpack"],