mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-11 20:43:42 +08:00
Add support for MCP in export mode
This commit is contained in:
@@ -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<string, McpClientData>();
|
||||
const toolToClientMap = new Map<string, string>();
|
||||
|
||||
// 获取客户端状态
|
||||
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<void> {
|
||||
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<McpConfigData> {
|
||||
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<McpConfigData> {
|
||||
|
||||
// 更新 MCP 配置文件
|
||||
async function updateMcpConfig(config: McpConfigData): Promise<void> {
|
||||
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;
|
||||
|
@@ -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<Client> {
|
||||
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(
|
||||
{
|
||||
|
@@ -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<McpNotifications> = 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<string, string>;
|
||||
status?: "active" | "paused" | "error";
|
||||
}
|
||||
|
||||
export interface ServerSseConfig {
|
||||
type: "sse";
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
status?: "active" | "paused" | "error";
|
||||
}
|
||||
|
||||
export interface McpConfigData {
|
||||
enableMcp?: boolean;
|
||||
// MCP Server 的配置
|
||||
mcpServers: Record<string, ServerConfig>;
|
||||
}
|
||||
|
Reference in New Issue
Block a user