From d1baabae14f9066b29acb39eb95134bd19ac2d7f Mon Sep 17 00:00:00 2001 From: YISH Date: Thu, 8 May 2025 13:27:04 +0800 Subject: [PATCH 1/2] Add support for MCP in export mode --- app/components/mcp-market.tsx | 27 ++++----- app/config/build.ts | 11 ++++ app/mcp/actions.ts | 110 +++++++++++++++++++++++++++++----- app/mcp/client.ts | 50 +++++++++++----- app/mcp/types.ts | 34 ++++++++--- app/store/access.ts | 7 ++- app/typing.ts | 4 ++ next.config.mjs | 6 ++ 8 files changed, 198 insertions(+), 51 deletions(-) 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"], From 62d32f317d071efeadf12147570c83c75361e1af Mon Sep 17 00:00:00 2001 From: YISH Date: Thu, 8 May 2025 19:40:04 +0800 Subject: [PATCH 2/2] Split actions.ts into actions.server.ts and actions.client.ts --- app/mcp/actions.base.ts | 491 ++++++++++++++++++++++++++++++++++++ app/mcp/actions.client.ts | 1 + app/mcp/actions.server.ts | 2 + app/mcp/actions.ts | 505 ++++---------------------------------- 4 files changed, 542 insertions(+), 457 deletions(-) create mode 100644 app/mcp/actions.base.ts create mode 100644 app/mcp/actions.client.ts create mode 100644 app/mcp/actions.server.ts diff --git a/app/mcp/actions.base.ts b/app/mcp/actions.base.ts new file mode 100644 index 000000000..ee9776554 --- /dev/null +++ b/app/mcp/actions.base.ts @@ -0,0 +1,491 @@ +import { + createClient, + executeRequest, + listTools, + removeClient, +} from "./client"; +import { MCPClientLogger } from "./logger"; +import { + DEFAULT_MCP_CONFIG, + McpClientData, + McpConfigData, + McpRequestMessage, + ServerConfig, + ServerStatusResponse, +} from "./types"; + +const JSON_INDENT = 2; + +const logger = new MCPClientLogger("MCP Actions"); + +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"); + } +}; + +class ClientsMap extends Map { + toolToClientMap: Map; + constructor() { + super(); + this.toolToClientMap = new Map(); + } + + set(clientId: string, data: McpClientData): this { + super.set(clientId, data); + + if (data?.tools?.tools) { + for (const tool of data.tools.tools) { + if (tool.name) { + this.toolToClientMap.set(tool.name, clientId); + } + } + } else { + this.purgeToolMappings(clientId); + } + return this; + } + + delete(clientId: string): boolean { + const ret = clientsMap.delete(clientId); + this.purgeToolMappings(clientId); + return ret; + } + + clear(): void { + super.clear(); + this.toolToClientMap.clear(); + } + + getByToolName(toolName: string) { + const toolClientId = clientsMap.toolToClientMap.get(toolName); + if (toolClientId) { + return clientsMap.get(toolClientId); + } + } + + private purgeToolMappings(clientId: string) { + for (const [tool, mappedId] of this.toolToClientMap.entries()) { + if (mappedId === clientId) { + this.toolToClientMap.delete(tool); + } + } + } +} + +const clientsMap = new ClientsMap(); + +// 获取客户端状态 +export async function getClientsStatus(): Promise< + Record +> { + const config = await getMcpConfigFromFile(); + const result: Record = {}; + + for (const clientId of Object.keys(config.mcpServers)) { + const status = clientsMap.get(clientId); + const serverConfig = config.mcpServers[clientId]; + + if (!serverConfig) { + result[clientId] = { status: "undefined", errorMsg: null }; + continue; + } + + if (serverConfig.status === "paused") { + result[clientId] = { status: "paused", errorMsg: null }; + continue; + } + + if (!status) { + result[clientId] = { status: "undefined", errorMsg: null }; + continue; + } + + if ( + status.client === null && + status.tools === null && + status.errorMsg === null + ) { + result[clientId] = { status: "initializing", errorMsg: null }; + continue; + } + + if (status.errorMsg) { + result[clientId] = { status: "error", errorMsg: status.errorMsg }; + continue; + } + + if (status.client) { + result[clientId] = { status: "active", errorMsg: null }; + continue; + } + + result[clientId] = { status: "error", errorMsg: "Client not found" }; + } + + return result; +} + +// 获取客户端工具 +export async function getClientTools(clientId: string) { + return clientsMap.get(clientId)?.tools ?? null; +} + +// 获取可用客户端数量 +export async function getAvailableClientsCount() { + let count = 0; + clientsMap.forEach((map) => !map.errorMsg && count++); + return count; +} + +// 获取所有客户端工具 +export async function getAllTools() { + const result = []; + for (const [clientId, status] of clientsMap.entries()) { + result.push({ + clientId, + tools: status.tools, + }); + } + return result; +} + +// 初始化单个客户端 +async function initializeSingleClient( + clientId: string, + serverConfig: ServerConfig, +) { + // 如果服务器状态是暂停,则不初始化 + if (serverConfig.status === "paused") { + logger.info(`Skipping initialization for paused client [${clientId}]`); + return; + } + + logger.info(`Initializing client [${clientId}]...`); + + // 先设置初始化状态 + clientsMap.set(clientId, { + client: null, + tools: null, + errorMsg: null, // null 表示正在初始化 + }); + + // 异步初始化 + createClient(clientId, serverConfig) + .then(async (client) => { + const tools = await listTools(client); + logger.info( + `Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`, + ); + clientsMap.set(clientId, { client, tools, errorMsg: null }); + logger.success(`Client [${clientId}] initialized successfully`); + }) + .catch((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 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)) { + await initializeSingleClient(clientId, serverConfig); + } + return config; + } catch (error) { + logger.error(`Failed to initialize MCP system: ${error}`); + throw error; + } +} + +// 添加服务器 +export async function addMcpServer(clientId: string, config: ServerConfig) { + try { + const currentConfig = await getMcpConfigFromFile(); + const isNewServer = !(clientId in currentConfig.mcpServers); + + // 如果是新服务器,设置默认状态为 active + if (isNewServer && !config.status) { + config.status = "active"; + } + + const newConfig = { + ...currentConfig, + mcpServers: { + ...currentConfig.mcpServers, + [clientId]: config, + }, + }; + await updateMcpConfig(newConfig); + + // 只有新服务器或状态为 active 的服务器才初始化 + if (isNewServer || config.status === "active") { + await initializeSingleClient(clientId, config); + } + + return newConfig; + } catch (error) { + logger.error(`Failed to add server [${clientId}]: ${error}`); + throw error; + } +} + +// 暂停服务器 +export async function pauseMcpServer(clientId: string) { + try { + const currentConfig = await getMcpConfigFromFile(); + const serverConfig = currentConfig.mcpServers[clientId]; + if (!serverConfig) { + throw new Error(`Server ${clientId} not found`); + } + + // 先更新配置 + const newConfig: McpConfigData = { + ...currentConfig, + mcpServers: { + ...currentConfig.mcpServers, + [clientId]: { + ...serverConfig, + status: "paused", + }, + }, + }; + await updateMcpConfig(newConfig); + + // 然后关闭客户端 + const client = clientsMap.get(clientId); + if (client?.client) { + await removeClient(client.client); + } + clientsMap.delete(clientId); + + return newConfig; + } catch (error) { + logger.error(`Failed to pause server [${clientId}]: ${error}`); + throw error; + } +} + +// 恢复服务器 +export async function resumeMcpServer(clientId: string): Promise { + try { + const currentConfig = await getMcpConfigFromFile(); + const serverConfig = currentConfig.mcpServers[clientId]; + if (!serverConfig) { + throw new Error(`Server ${clientId} not found`); + } + + // 先尝试初始化客户端 + logger.info(`Trying to initialize client [${clientId}]...`); + try { + const client = await createClient(clientId, serverConfig); + const tools = await listTools(client); + clientsMap.set(clientId, { client, tools, errorMsg: null }); + logger.success(`Client [${clientId}] initialized successfully`); + + // 初始化成功后更新配置 + const newConfig: McpConfigData = { + ...currentConfig, + mcpServers: { + ...currentConfig.mcpServers, + [clientId]: { + ...serverConfig, + status: "active" as const, + }, + }, + }; + await updateMcpConfig(newConfig); + } catch (error) { + const currentConfig = await getMcpConfigFromFile(); + const serverConfig = currentConfig.mcpServers[clientId]; + + // 如果配置中存在该服务器,则更新其状态为 error + if (serverConfig) { + serverConfig.status = "error"; + await updateMcpConfig(currentConfig); + } + + // 初始化失败 + clientsMap.set(clientId, { + client: null, + tools: null, + errorMsg: error instanceof Error ? error.message : String(error), + }); + logger.error(`Failed to initialize client [${clientId}]: ${error}`); + throw error; + } + } catch (error) { + logger.error(`Failed to resume server [${clientId}]: ${error}`); + throw 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); + } + clientsMap.delete(clientId); + + return newConfig; + } catch (error) { + logger.error(`Failed to remove server [${clientId}]: ${error}`); + throw error; + } +} + +// 重启所有客户端 +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(); + + // 重新初始化 + 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; + } +} + +// 执行 MCP 请求 +export async function executeMcpAction( + clientId: string, + request: McpRequestMessage, +) { + try { + let client = clientsMap.get(clientId); + if ( + !client && + request.params?.name && + typeof request.params.name === "string" + ) { + client = clientsMap.getByToolName(request.params.name); + } + if (!client?.client) { + throw new Error(`Client ${clientId} not found`); + } + logger.info(`Executing request for [${clientId}]`); + return await executeRequest(client.client, request); + } catch (error) { + logger.error(`Failed to execute request for [${clientId}]: ${error}`); + throw error; + } +} + +// 获取 MCP 配置文件 +export async function getMcpConfigFromFile(): Promise { + try { + 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; + } +} + +// 更新 MCP 配置文件 +async function updateMcpConfig(config: McpConfigData): Promise { + 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(await getConfigPath()), { recursive: true }); + await fs.writeFile( + await getConfigPath(), + JSON.stringify(config, null, JSON_INDENT), + ); + } +} + +// 检查 MCP 是否启用 +export async function isMcpEnabled() { + try { + 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/actions.client.ts b/app/mcp/actions.client.ts new file mode 100644 index 000000000..e42cff35d --- /dev/null +++ b/app/mcp/actions.client.ts @@ -0,0 +1 @@ +export * from "./actions.base"; diff --git a/app/mcp/actions.server.ts b/app/mcp/actions.server.ts new file mode 100644 index 000000000..bc4b4dc4f --- /dev/null +++ b/app/mcp/actions.server.ts @@ -0,0 +1,2 @@ +"use server"; +export * from "./actions.base"; diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index e330d2e93..1c603b104 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -1,465 +1,56 @@ -if (!EXPORT_MODE) { - ("use server"); -} -import { - createClient, - executeRequest, - listTools, - removeClient, -} from "./client"; -import { MCPClientLogger } from "./logger"; -import { - DEFAULT_MCP_CONFIG, - McpClientData, - McpConfigData, - McpRequestMessage, - ServerConfig, - ServerStatusResponse, -} from "./types"; +import { McpRequestMessage, ServerConfig } from "./types"; -const JSON_INDENT = 2; +let actionsHost: typeof import("./actions.base") | undefined; -const logger = new MCPClientLogger("MCP Actions"); - -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 actions = async () => { + if (!actionsHost) { + if (EXPORT_MODE) { + actionsHost = await import("./actions.client"); + } else { + actionsHost = await import("./actions.server"); + } } + + return actionsHost; }; -const clientsMap = new Map(); -const toolToClientMap = new Map(); +export const getAvailableClientsCount = async () => { + return (await actions()).getAvailableClientsCount(); +}; +export const isMcpEnabled = async () => { + return (await actions()).isMcpEnabled(); +}; +export const initializeMcpSystem = async () => { + return (await actions()).initializeMcpSystem(); +}; +export const addMcpServer = async (clientId: string, config: ServerConfig) => { + return (await actions()).addMcpServer(clientId, config); +}; -// 获取客户端状态 -export async function getClientsStatus(): Promise< - Record -> { - const config = await getMcpConfigFromFile(); - const result: Record = {}; - - for (const clientId of Object.keys(config.mcpServers)) { - const status = clientsMap.get(clientId); - const serverConfig = config.mcpServers[clientId]; - - if (!serverConfig) { - result[clientId] = { status: "undefined", errorMsg: null }; - continue; - } - - if (serverConfig.status === "paused") { - result[clientId] = { status: "paused", errorMsg: null }; - continue; - } - - if (!status) { - result[clientId] = { status: "undefined", errorMsg: null }; - continue; - } - - if ( - status.client === null && - status.tools === null && - status.errorMsg === null - ) { - result[clientId] = { status: "initializing", errorMsg: null }; - continue; - } - - if (status.errorMsg) { - result[clientId] = { status: "error", errorMsg: status.errorMsg }; - continue; - } - - if (status.client) { - result[clientId] = { status: "active", errorMsg: null }; - continue; - } - - result[clientId] = { status: "error", errorMsg: "Client not found" }; - } - - return result; -} - -// 获取客户端工具 -export async function getClientTools(clientId: string) { - return clientsMap.get(clientId)?.tools ?? null; -} - -// 获取可用客户端数量 -export async function getAvailableClientsCount() { - let count = 0; - clientsMap.forEach((map) => !map.errorMsg && count++); - return count; -} - -// 获取所有客户端工具 -export async function getAllTools() { - const result = []; - for (const [clientId, status] of clientsMap.entries()) { - result.push({ - clientId, - tools: status.tools, - }); - } - return result; -} - -// 初始化单个客户端 -async function initializeSingleClient( - clientId: string, - serverConfig: ServerConfig, -) { - // 如果服务器状态是暂停,则不初始化 - if (serverConfig.status === "paused") { - logger.info(`Skipping initialization for paused client [${clientId}]`); - return; - } - - logger.info(`Initializing client [${clientId}]...`); - - // 先设置初始化状态 - clientsMap.set(clientId, { - client: null, - tools: null, - errorMsg: null, // null 表示正在初始化 - }); - - // 异步初始化 - createClient(clientId, serverConfig) - .then(async (client) => { - const tools = await listTools(client); - logger.info( - `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) => { - 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 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)) { - await initializeSingleClient(clientId, serverConfig); - } - return config; - } catch (error) { - logger.error(`Failed to initialize MCP system: ${error}`); - throw error; - } -} - -// 添加服务器 -export async function addMcpServer(clientId: string, config: ServerConfig) { - try { - const currentConfig = await getMcpConfigFromFile(); - const isNewServer = !(clientId in currentConfig.mcpServers); - - // 如果是新服务器,设置默认状态为 active - if (isNewServer && !config.status) { - config.status = "active"; - } - - const newConfig = { - ...currentConfig, - mcpServers: { - ...currentConfig.mcpServers, - [clientId]: config, - }, - }; - await updateMcpConfig(newConfig); - - // 只有新服务器或状态为 active 的服务器才初始化 - if (isNewServer || config.status === "active") { - await initializeSingleClient(clientId, config); - } - - return newConfig; - } catch (error) { - logger.error(`Failed to add server [${clientId}]: ${error}`); - throw error; - } -} - -// 暂停服务器 -export async function pauseMcpServer(clientId: string) { - try { - const currentConfig = await getMcpConfigFromFile(); - const serverConfig = currentConfig.mcpServers[clientId]; - if (!serverConfig) { - throw new Error(`Server ${clientId} not found`); - } - - // 先更新配置 - const newConfig: McpConfigData = { - ...currentConfig, - mcpServers: { - ...currentConfig.mcpServers, - [clientId]: { - ...serverConfig, - status: "paused", - }, - }, - }; - await updateMcpConfig(newConfig); - - // 然后关闭客户端 - const client = clientsMap.get(clientId); - if (client?.client) { - await removeClient(client.client); - } - clientsMap.delete(clientId); - - return newConfig; - } catch (error) { - logger.error(`Failed to pause server [${clientId}]: ${error}`); - throw error; - } -} - -// 恢复服务器 -export async function resumeMcpServer(clientId: string): Promise { - try { - const currentConfig = await getMcpConfigFromFile(); - const serverConfig = currentConfig.mcpServers[clientId]; - if (!serverConfig) { - throw new Error(`Server ${clientId} not found`); - } - - // 先尝试初始化客户端 - logger.info(`Trying to initialize client [${clientId}]...`); - try { - 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`); - - // 初始化成功后更新配置 - const newConfig: McpConfigData = { - ...currentConfig, - mcpServers: { - ...currentConfig.mcpServers, - [clientId]: { - ...serverConfig, - status: "active" as const, - }, - }, - }; - await updateMcpConfig(newConfig); - } catch (error) { - const currentConfig = await getMcpConfigFromFile(); - const serverConfig = currentConfig.mcpServers[clientId]; - - // 如果配置中存在该服务器,则更新其状态为 error - if (serverConfig) { - serverConfig.status = "error"; - await updateMcpConfig(currentConfig); - } - - // 初始化失败 - clientsMap.set(clientId, { - client: null, - tools: null, - errorMsg: error instanceof Error ? error.message : String(error), - }); - logger.error(`Failed to initialize client [${clientId}]: ${error}`); - throw error; - } - } catch (error) { - logger.error(`Failed to resume server [${clientId}]: ${error}`); - throw 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); - } - clientsMap.delete(clientId); - - return newConfig; - } catch (error) { - logger.error(`Failed to remove server [${clientId}]: ${error}`); - throw error; - } -} - -// 重启所有客户端 -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(); - - // 重新初始化 - 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; - } -} - -// 执行 MCP 请求 -export async function executeMcpAction( +export const getClientsStatus = async () => { + return (await actions()).getClientsStatus(); +}; +export const getClientTools = async (clientId: string) => { + return (await actions()).getClientTools(clientId); +}; +export const getMcpConfigFromFile = async () => { + return (await actions()).getMcpConfigFromFile(); +}; +export const pauseMcpServer = async (clientId: string) => { + return (await actions()).pauseMcpServer(clientId); +}; +export const restartAllClients = async () => { + return (await actions()).restartAllClients(); +}; +export const resumeMcpServer = async (clientId: string) => { + return (await actions()).resumeMcpServer(clientId); +}; +export const executeMcpAction = async ( clientId: string, request: McpRequestMessage, -) { - try { - 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`); - } - logger.info(`Executing request for [${clientId}]`); - return await executeRequest(client.client, request); - } catch (error) { - logger.error(`Failed to execute request for [${clientId}]: ${error}`); - throw error; - } -} - -// 获取 MCP 配置文件 -export async function getMcpConfigFromFile(): Promise { - try { - 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; - } -} - -// 更新 MCP 配置文件 -async function updateMcpConfig(config: McpConfigData): Promise { - 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(await getConfigPath()), { recursive: true }); - await fs.writeFile( - await getConfigPath(), - JSON.stringify(config, null, JSON_INDENT), - ); - } -} - -// 检查 MCP 是否启用 -export async function isMcpEnabled() { - try { - 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; - } -} +) => { + return (await actions()).executeMcpAction(clientId, request); +}; +export const getAllTools = async () => { + return (await actions()).getAllTools(); +};