feat: Optimize MCP configuration logic

This commit is contained in:
Kadxy
2025-01-15 16:52:54 +08:00
parent ce13cf61a7
commit 8aa9a500fd
14 changed files with 766 additions and 653 deletions

View File

@@ -1,236 +1,217 @@
"use server";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import {
createClient,
executeRequest,
listPrimitives,
Primitive,
listTools,
removeClient,
} from "./client";
import { MCPClientLogger } from "./logger";
import { McpRequestMessage, McpConfig, ServerConfig } from "./types";
import {
DEFAULT_MCP_CONFIG,
McpClientData,
McpConfigData,
McpRequestMessage,
ServerConfig,
} from "./types";
import fs from "fs/promises";
import path from "path";
const logger = new MCPClientLogger("MCP Actions");
// Use Map to store all clients
const clientsMap = new Map<
string,
{ client: Client | null; primitives: Primitive[]; errorMsg: string | null }
>();
// Whether initialized
let initialized = false;
// Store failed clients
let errorClients: string[] = [];
const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
// 获取 MCP 配置
export async function getMcpConfig(): Promise<McpConfig> {
try {
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
return JSON.parse(configStr);
} catch (error) {
console.error("Failed to read MCP config:", error);
return { mcpServers: {} };
}
const clientsMap = new Map<string, McpClientData>();
// 获取客户端状态
export async function getClientStatus(clientId: string) {
const status = clientsMap.get(clientId);
if (!status) return { status: "undefined" as const, errorMsg: null };
return {
status: status.errorMsg ? ("error" as const) : ("active" as const),
errorMsg: status.errorMsg,
};
}
// 更新 MCP 配置
export async function updateMcpConfig(config: McpConfig): Promise<void> {
try {
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
} catch (error) {
console.error("Failed to write MCP config:", error);
throw error;
}
// 获取客户端工具
export async function getClientTools(clientId: string) {
return clientsMap.get(clientId)?.tools ?? null;
}
// 重新初始化所有客户端
export async function reinitializeMcpClients() {
logger.info("Reinitializing MCP clients...");
// 遍历所有客户端,关闭
try {
for (const [clientId, clientData] of clientsMap.entries()) {
clientData.client?.close();
// 获取可用客户端数量
export async function getAvailableClientsCount() {
let count = 0;
clientsMap.forEach((map) => {
if (!map.errorMsg) {
count += map?.tools?.tools?.length ?? 0;
}
} catch (error) {
logger.error(`Failed to close clients: ${error}`);
}
// 清空状态
clientsMap.clear();
errorClients = [];
initialized = false;
// 重新初始化
return initializeMcpClients();
});
return count;
}
// Initialize all configured clients
export async function initializeMcpClients() {
// If already initialized, return
if (initialized) {
return { errorClients };
// 获取所有客户端工具
export async function getAllTools() {
const result = [];
for (const [clientId, status] of clientsMap.entries()) {
result.push({
clientId,
tools: status.tools,
});
}
logger.info("Starting to initialize MCP clients...");
errorClients = [];
const config = await getMcpConfig();
// Initialize all clients, key is clientId, value is client config
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
try {
logger.info(`Initializing MCP client: ${clientId}`);
const client = await createClient(serverConfig as ServerConfig, clientId);
const primitives = await listPrimitives(client);
clientsMap.set(clientId, { client, primitives, errorMsg: null });
logger.success(
`Client [${clientId}] initialized, ${primitives.length} primitives supported`,
);
} catch (error) {
errorClients.push(clientId);
clientsMap.set(clientId, {
client: null,
primitives: [],
errorMsg: error instanceof Error ? error.message : String(error),
});
logger.error(`Failed to initialize client ${clientId}: ${error}`);
}
}
initialized = true;
if (errorClients.length > 0) {
logger.warn(`Failed to initialize clients: ${errorClients.join(", ")}`);
} else {
logger.success("All MCP clients initialized");
}
const availableClients = await getAvailableClients();
logger.info(`Available clients: ${availableClients.join(",")}`);
return { errorClients };
return result;
}
// Execute MCP request
export async function executeMcpAction(
// 初始化单个客户端
async function initializeSingleClient(
clientId: string,
request: McpRequestMessage,
serverConfig: ServerConfig,
) {
logger.info(`Initializing client [${clientId}]...`);
try {
// Find the corresponding client
const client = clientsMap.get(clientId)?.client;
if (!client) {
logger.error(`Client ${clientId} not found`);
return;
}
logger.info(`Executing MCP request for ${clientId}`);
// Execute request and return result
return await executeRequest(client, request);
const client = await createClient(clientId, serverConfig);
const tools = await listTools(client);
clientsMap.set(clientId, { client, tools, errorMsg: null });
logger.success(`Client [${clientId}] initialized successfully`);
} catch (error) {
logger.error(`MCP execution error: ${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 {
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;
}
}
// Get all available client IDs
export async function getAvailableClients() {
return Array.from(clientsMap.entries())
.filter(([_, data]) => data.errorMsg === null)
.map(([clientId]) => clientId);
}
// Get all primitives from all clients
export async function getAllPrimitives(): Promise<
{
clientId: string;
primitives: Primitive[];
}[]
> {
return Array.from(clientsMap.entries()).map(([clientId, { primitives }]) => ({
clientId,
primitives,
}));
}
// 获取客户端的 Primitives
export async function getClientPrimitives(clientId: string) {
// 添加服务器
export async function addMcpServer(clientId: string, config: ServerConfig) {
try {
const clientData = clientsMap.get(clientId);
if (!clientData) {
console.warn(`Client ${clientId} not found in map`);
return null;
}
if (clientData.errorMsg) {
console.warn(`Client ${clientId} has error: ${clientData.errorMsg}`);
return null;
}
return clientData.primitives;
const currentConfig = await getMcpConfigFromFile();
const newConfig = {
...currentConfig,
mcpServers: {
...currentConfig.mcpServers,
[clientId]: config,
},
};
await updateMcpConfig(newConfig);
// 只初始化新添加的服务器
await initializeSingleClient(clientId, config);
return newConfig;
} catch (error) {
console.error(`Failed to get primitives for client ${clientId}:`, error);
return null;
logger.error(`Failed to add 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 MCP clients...");
// 清空状态
clientsMap.clear();
errorClients = [];
initialized = false;
// 重新初始化
await initializeMcpClients();
return {
success: errorClients.length === 0,
errorClients,
};
}
// 获取所有客户端状态
export async function getAllClientStatus(): Promise<
Record<string, string | null>
> {
const status: Record<string, string | null> = {};
for (const [clientId, data] of clientsMap.entries()) {
status[clientId] = data.errorMsg;
}
return status;
}
// 检查客户端状态
export async function getClientErrors(): Promise<
Record<string, string | null>
> {
const errors: Record<string, string | null> = {};
for (const [clientId, data] of clientsMap.entries()) {
errors[clientId] = data.errorMsg;
}
return errors;
}
// 获取客户端状态,不重新初始化
export async function refreshClientStatus() {
logger.info("Refreshing client status...");
// 如果还没初始化过,则初始化
if (!initialized) {
return initializeMcpClients();
}
// 否则只更新错误状态
errorClients = [];
for (const [clientId, clientData] of clientsMap.entries()) {
if (clientData.errorMsg !== null) {
errorClients.push(clientId);
logger.info("Restarting all clients...");
try {
// 关闭所有客户端
for (const client of clientsMap.values()) {
if (client.client) {
await removeClient(client.client);
}
}
}
// 清空状态
clientsMap.clear();
return { errorClients };
// 重新初始化
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 {
const client = clientsMap.get(clientId);
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 {
const configStr = await fs.readFile(CONFIG_PATH, "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 {
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
} catch (error) {
throw error;
}
}
// 重新初始化单个客户端
export async function reinitializeClient(clientId: string) {
const config = await getMcpConfigFromFile();
const serverConfig = config.mcpServers[clientId];
if (!serverConfig) {
throw new Error(`Server config not found for client ${clientId}`);
}
await initializeSingleClient(clientId, serverConfig);
}

View File

@@ -1,85 +1,45 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { MCPClientLogger } from "./logger";
import { McpRequestMessage } from "./types";
import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types";
import { z } from "zod";
export interface ServerConfig {
command: string;
args?: string[];
env?: Record<string, string>;
}
const logger = new MCPClientLogger();
export async function createClient(
serverConfig: ServerConfig,
name: string,
id: string,
config: ServerConfig,
): Promise<Client> {
logger.info(`Creating client for server ${name}`);
logger.info(`Creating client for ${id}...`);
const transport = new StdioClientTransport({
command: serverConfig.command,
args: serverConfig.args,
env: serverConfig.env,
command: config.command,
args: config.args,
env: config.env,
});
const client = new Client(
{
name: `nextchat-mcp-client-${name}`,
name: `nextchat-mcp-client-${id}`,
version: "1.0.0",
},
{
capabilities: {
// roots: {
// listChanged: true,
// },
},
capabilities: {},
},
);
await client.connect(transport);
return client;
}
export interface Primitive {
type: "resource" | "tool" | "prompt";
value: any;
export async function removeClient(client: Client) {
logger.info(`Removing client...`);
await client.close();
}
/** List all resources, tools, and prompts */
export async function listPrimitives(client: Client): Promise<Primitive[]> {
const capabilities = client.getServerCapabilities();
const primitives: Primitive[] = [];
const promises = [];
if (capabilities?.resources) {
promises.push(
client.listResources().then(({ resources }) => {
resources.forEach((item) =>
primitives.push({ type: "resource", value: item }),
);
}),
);
}
if (capabilities?.tools) {
promises.push(
client.listTools().then(({ tools }) => {
tools.forEach((item) => primitives.push({ type: "tool", value: item }));
}),
);
}
if (capabilities?.prompts) {
promises.push(
client.listPrompts().then(({ prompts }) => {
prompts.forEach((item) =>
primitives.push({ type: "prompt", value: item }),
);
}),
);
}
await Promise.all(promises);
return primitives;
export async function listTools(client: Client): Promise<ListToolsResponse> {
return client.listTools();
}
/** Execute a request */
export async function executeRequest(
client: Client,
request: McpRequestMessage,

View File

@@ -1,27 +1,23 @@
import { createClient, listPrimitives } from "@/app/mcp/client";
import { createClient, listTools } from "@/app/mcp/client";
import { MCPClientLogger } from "@/app/mcp/logger";
import conf from "./mcp_config.json";
const logger = new MCPClientLogger("MCP Server Example", true);
const TEST_SERVER = "everything";
const TEST_SERVER = "filesystem";
async function main() {
logger.info(`All MCP servers: ${Object.keys(conf.mcpServers).join(", ")}`);
logger.info(`Connecting to server ${TEST_SERVER}...`);
const client = await createClient(conf.mcpServers[TEST_SERVER], TEST_SERVER);
const primitives = await listPrimitives(client);
const client = await createClient(TEST_SERVER, conf.mcpServers[TEST_SERVER]);
const tools = await listTools(client);
logger.success(`Connected to server ${TEST_SERVER}`);
logger.info(
`${TEST_SERVER} supported primitives:\n${JSON.stringify(
primitives.filter((i) => i.type === "tool"),
null,
2,
)}`,
`${TEST_SERVER} supported primitives:\n${JSON.stringify(tools, null, 2)}`,
);
}

View File

@@ -1,3 +1,12 @@
{
"mcpServers": {}
}
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"."
]
}
}
}

View File

@@ -2,7 +2,9 @@
{
"id": "filesystem",
"name": "Filesystem",
"description": "Secure file operations with configurable access controls",
"description": "Secure file operations with configurable access controlsSecure file operations with configurable access controlsSecure file operations with configurable access controlsSecure file operations with configurable access controls",
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem",
"tags": ["filesystem", "storage", "local"],
"command": "npx",
"baseArgs": ["-y", "@modelcontextprotocol/server-filesystem"],
"configurable": true,
@@ -12,7 +14,9 @@
"type": "array",
"description": "Allowed file system paths",
"required": true,
"minItems": 1
"minItems": 1,
"itemLabel": "Path",
"addButtonText": "Add Path"
}
}
},
@@ -27,6 +31,8 @@
"id": "github",
"name": "GitHub",
"description": "Repository management, file operations, and GitHub API integration",
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/github",
"tags": ["github", "git", "api", "vcs"],
"command": "npx",
"baseArgs": ["-y", "@modelcontextprotocol/server-github"],
"configurable": true,
@@ -50,6 +56,8 @@
"id": "gdrive",
"name": "Google Drive",
"description": "File access and search capabilities for Google Drive",
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive",
"tags": ["google", "drive", "storage", "cloud"],
"command": "npx",
"baseArgs": ["-y", "@modelcontextprotocol/server-gdrive"],
"configurable": false
@@ -58,6 +66,8 @@
"id": "playwright",
"name": "Playwright",
"description": "Browser automation and webscrapping with Playwright",
"repo": "https://github.com/executeautomation/mcp-playwright",
"tags": ["browser", "automation", "scraping"],
"command": "npx",
"baseArgs": ["-y", "@executeautomation/playwright-mcp-server"],
"configurable": false
@@ -66,6 +76,8 @@
"id": "mongodb",
"name": "MongoDB",
"description": "Direct interaction with MongoDB databases",
"repo": "",
"tags": ["database", "mongodb", "nosql"],
"command": "node",
"baseArgs": ["dist/index.js"],
"configurable": true,
@@ -89,6 +101,8 @@
"id": "difyworkflow",
"name": "Dify Workflow",
"description": "Tools to query and execute Dify workflows",
"repo": "https://github.com/gotoolkits/mcp-difyworkflow-server",
"tags": ["workflow", "automation", "dify"],
"command": "mcp-difyworkflow-server",
"baseArgs": ["-base-url"],
"configurable": true,
@@ -130,6 +144,8 @@
"id": "postgres",
"name": "PostgreSQL",
"description": "Read-only database access with schema inspection",
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres",
"tags": ["database", "postgresql", "sql"],
"command": "docker",
"baseArgs": ["run", "-i", "--rm", "mcp/postgres"],
"configurable": true,
@@ -153,6 +169,8 @@
"id": "brave-search",
"name": "Brave Search",
"description": "Web and local search using Brave's Search API",
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search",
"tags": ["search", "brave", "api"],
"command": "npx",
"baseArgs": ["-y", "@modelcontextprotocol/server-brave-search"],
"configurable": true,
@@ -176,6 +194,8 @@
"id": "google-maps",
"name": "Google Maps",
"description": "Location services, directions, and place details",
"repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps",
"tags": ["maps", "google", "location", "api"],
"command": "npx",
"baseArgs": ["-y", "@modelcontextprotocol/server-google-maps"],
"configurable": true,
@@ -199,6 +219,8 @@
"id": "docker-mcp",
"name": "Docker",
"description": "Run and manage docker containers, docker compose, and logs",
"repo": "https://github.com/QuantGeekDev/docker-mcp",
"tags": ["docker", "container", "devops"],
"command": "uvx",
"baseArgs": ["docker-mcp"],
"configurable": false

View File

@@ -1,6 +1,7 @@
// ref: https://spec.modelcontextprotocol.io/specification/basic/messages/
import { z } from "zod";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
export interface McpRequestMessage {
jsonrpc?: "2.0";
@@ -60,6 +61,32 @@ export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
params: z.record(z.unknown()).optional(),
});
////////////
// Next Chat
////////////
export interface ListToolsResponse {
tools: {
name?: string;
description?: string;
inputSchema?: object;
[key: string]: any;
};
}
export type McpClientData = McpActiveClient | McpErrorClient;
interface McpActiveClient {
client: Client;
tools: ListToolsResponse;
errorMsg: null;
}
interface McpErrorClient {
client: null;
tools: null;
errorMsg: string;
}
// MCP 服务器配置相关类型
export interface ServerConfig {
command: string;
@@ -67,23 +94,52 @@ export interface ServerConfig {
env?: Record<string, string>;
}
export interface McpConfig {
export interface McpConfigData {
// MCP Server 的配置
mcpServers: Record<string, ServerConfig>;
}
export const DEFAULT_MCP_CONFIG: McpConfigData = {
mcpServers: {},
};
export interface ArgsMapping {
// 参数映射的类型
type: "spread" | "single" | "env";
// 参数映射的位置
position?: number;
// 参数映射的 key
key?: string;
}
export interface PresetServer {
// MCP Server 的唯一标识,作为最终配置文件 Json 的 key
id: string;
// MCP Server 的显示名称
name: string;
// MCP Server 的描述
description: string;
// MCP Server 的仓库地址
repo: string;
// MCP Server 的标签
tags: string[];
// MCP Server 的命令
command: string;
// MCP Server 的参数
baseArgs: string[];
// MCP Server 是否需要配置
configurable: boolean;
// MCP Server 的配置 schema
configSchema?: {
properties: Record<
string,
@@ -95,5 +151,7 @@ export interface PresetServer {
}
>;
};
// MCP Server 的参数映射
argsMapping?: Record<string, ArgsMapping>;
}