mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-17 23:43:41 +08:00
Compare commits
3 Commits
9f969903ed
...
b72013e9c3
Author | SHA1 | Date | |
---|---|---|---|
|
b72013e9c3 | ||
|
62d32f317d | ||
|
d1baabae14 |
@@ -22,7 +22,6 @@ import {
|
|||||||
resumeMcpServer,
|
resumeMcpServer,
|
||||||
} from "../mcp/actions";
|
} from "../mcp/actions";
|
||||||
import {
|
import {
|
||||||
ListToolsResponse,
|
|
||||||
ToolSchema,
|
ToolSchema,
|
||||||
McpConfigData,
|
McpConfigData,
|
||||||
PresetServer,
|
PresetServer,
|
||||||
@@ -48,7 +47,7 @@ export function McpMarketPage() {
|
|||||||
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>();
|
||||||
const [tools, setTools] = useState<ListToolsResponse | null>(null);
|
const [tools, setTools] = useState<ToolSchema[] | null>(null);
|
||||||
const [viewingServerId, setViewingServerId] = useState<string | undefined>();
|
const [viewingServerId, setViewingServerId] = useState<string | undefined>();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [config, setConfig] = useState<McpConfigData>();
|
const [config, setConfig] = useState<McpConfigData>();
|
||||||
@@ -232,7 +231,7 @@ export function McpMarketPage() {
|
|||||||
try {
|
try {
|
||||||
const result = await getClientTools(id);
|
const result = await getClientTools(id);
|
||||||
if (result) {
|
if (result) {
|
||||||
setTools(result);
|
setTools(result?.tools);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Failed to load tools");
|
throw new Error("Failed to load tools");
|
||||||
}
|
}
|
||||||
@@ -733,8 +732,8 @@ export function McpMarketPage() {
|
|||||||
<div className={styles["tools-list"]}>
|
<div className={styles["tools-list"]}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
) : tools?.tools ? (
|
) : tools ? (
|
||||||
tools.tools.map((tool: ToolSchema, index: number) => (
|
tools.map((tool: ToolSchema, index: number) => (
|
||||||
<div key={index} className={styles["tool-item"]}>
|
<div key={index} className={styles["tool-item"]}>
|
||||||
<div className={styles["tool-name"]}>{tool.name}</div>
|
<div className={styles["tool-name"]}>{tool.name}</div>
|
||||||
<div className={styles["tool-description"]}>
|
<div className={styles["tool-description"]}>
|
||||||
|
491
app/mcp/actions.base.ts
Normal file
491
app/mcp/actions.base.ts
Normal file
@@ -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<string, McpClientData> {
|
||||||
|
toolToClientMap: Map<string, string>;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.toolToClientMap = new Map<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, ServerStatusResponse>
|
||||||
|
> {
|
||||||
|
const config = await getMcpConfigFromFile();
|
||||||
|
const result: Record<string, ServerStatusResponse> = {};
|
||||||
|
|
||||||
|
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<void> {
|
||||||
|
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<McpConfigData> {
|
||||||
|
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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
1
app/mcp/actions.client.ts
Normal file
1
app/mcp/actions.client.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./actions.base";
|
2
app/mcp/actions.server.ts
Normal file
2
app/mcp/actions.server.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"use server";
|
||||||
|
export * from "./actions.base";
|
@@ -1,447 +1,56 @@
|
|||||||
if (!EXPORT_MODE) {
|
import { McpRequestMessage, ServerConfig } from "./types";
|
||||||
("use server");
|
|
||||||
}
|
|
||||||
import {
|
|
||||||
createClient,
|
|
||||||
executeRequest,
|
|
||||||
listTools,
|
|
||||||
removeClient,
|
|
||||||
} from "./client";
|
|
||||||
import { MCPClientLogger } from "./logger";
|
|
||||||
import {
|
|
||||||
DEFAULT_MCP_CONFIG,
|
|
||||||
McpClientData,
|
|
||||||
McpConfigData,
|
|
||||||
McpRequestMessage,
|
|
||||||
ServerConfig,
|
|
||||||
ServerStatusResponse,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
const logger = new MCPClientLogger("MCP Actions");
|
let actionsHost: typeof import("./actions.base") | undefined;
|
||||||
|
|
||||||
const getConfigPath = async () => {
|
const actions = async () => {
|
||||||
|
if (!actionsHost) {
|
||||||
if (EXPORT_MODE) {
|
if (EXPORT_MODE) {
|
||||||
return "/mcp/config.json";
|
actionsHost = await import("./actions.client");
|
||||||
} else {
|
} else {
|
||||||
const path = await import("path");
|
actionsHost = await import("./actions.server");
|
||||||
return path.join(process.cwd(), "app/mcp/mcp_config.json");
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionsHost;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clientsMap = new Map<string, McpClientData>();
|
export const getAvailableClientsCount = async () => {
|
||||||
|
return (await actions()).getAvailableClientsCount();
|
||||||
// 获取客户端状态
|
|
||||||
export async function getClientsStatus(): Promise<
|
|
||||||
Record<string, ServerStatusResponse>
|
|
||||||
> {
|
|
||||||
const config = await getMcpConfigFromFile();
|
|
||||||
const result: Record<string, ServerStatusResponse> = {};
|
|
||||||
|
|
||||||
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);
|
export const isMcpEnabled = async () => {
|
||||||
|
return (await actions()).isMcpEnabled();
|
||||||
// 只有新服务器或状态为 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);
|
export const initializeMcpSystem = async () => {
|
||||||
|
return (await actions()).initializeMcpSystem();
|
||||||
// 然后关闭客户端
|
|
||||||
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<void> {
|
|
||||||
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);
|
export const addMcpServer = async (clientId: string, config: ServerConfig) => {
|
||||||
} catch (error) {
|
return (await actions()).addMcpServer(clientId, config);
|
||||||
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);
|
|
||||||
|
|
||||||
// 关闭并移除客户端
|
export const getClientsStatus = async () => {
|
||||||
const client = clientsMap.get(clientId);
|
return (await actions()).getClientsStatus();
|
||||||
if (client?.client) {
|
};
|
||||||
await removeClient(client.client);
|
export const getClientTools = async (clientId: string) => {
|
||||||
}
|
return (await actions()).getClientTools(clientId);
|
||||||
clientsMap.delete(clientId);
|
};
|
||||||
|
export const getMcpConfigFromFile = async () => {
|
||||||
return newConfig;
|
return (await actions()).getMcpConfigFromFile();
|
||||||
} catch (error) {
|
};
|
||||||
logger.error(`Failed to remove server [${clientId}]: ${error}`);
|
export const pauseMcpServer = async (clientId: string) => {
|
||||||
throw error;
|
return (await actions()).pauseMcpServer(clientId);
|
||||||
}
|
};
|
||||||
}
|
export const restartAllClients = async () => {
|
||||||
|
return (await actions()).restartAllClients();
|
||||||
// 重启所有客户端
|
};
|
||||||
export async function restartAllClients() {
|
export const resumeMcpServer = async (clientId: string) => {
|
||||||
logger.info("Restarting all clients...");
|
return (await actions()).resumeMcpServer(clientId);
|
||||||
try {
|
};
|
||||||
// 关闭所有客户端
|
export const executeMcpAction = async (
|
||||||
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,
|
clientId: string,
|
||||||
request: McpRequestMessage,
|
request: McpRequestMessage,
|
||||||
) {
|
) => {
|
||||||
try {
|
return (await actions()).executeMcpAction(clientId, request);
|
||||||
let client = clientsMap.get(clientId);
|
};
|
||||||
if (!client) {
|
export const getAllTools = async () => {
|
||||||
client = [...clientsMap.values()].find(
|
return (await actions()).getAllTools();
|
||||||
(c) =>
|
};
|
||||||
c.tools?.tools &&
|
|
||||||
c.tools.tools.find((t) => t.name == 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<McpConfigData> {
|
|
||||||
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);
|
|
||||||
const merged = { ...config.mcpServers };
|
|
||||||
if (storedConfig.mcpServers) {
|
|
||||||
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<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(await getConfigPath()), { recursive: true });
|
|
||||||
await fs.writeFile(
|
|
||||||
await getConfigPath(),
|
|
||||||
JSON.stringify(config, null, 2),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -25,7 +25,9 @@ export async function createClient(
|
|||||||
transport = new SSEClientTransport(new URL(config.url));
|
transport = new SSEClientTransport(new URL(config.url));
|
||||||
} else {
|
} else {
|
||||||
if (EXPORT_MODE) {
|
if (EXPORT_MODE) {
|
||||||
throw new Error("Cannot use stdio transport in export mode");
|
throw new Error(
|
||||||
|
"Cannot use stdio transport in export mode. Please use SSE transport configuration instead.",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const { StdioClientTransport } = await import(
|
const { StdioClientTransport } = await import(
|
||||||
"@modelcontextprotocol/sdk/client/stdio.js"
|
"@modelcontextprotocol/sdk/client/stdio.js"
|
||||||
|
Reference in New Issue
Block a user