mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-08 10:17:05 +08:00
feat: MCP market
This commit is contained in:
@@ -7,15 +7,16 @@ import {
|
||||
Primitive,
|
||||
} from "./client";
|
||||
import { MCPClientLogger } from "./logger";
|
||||
import conf from "./mcp_config.json";
|
||||
import { McpRequestMessage } from "./types";
|
||||
import { McpRequestMessage, McpConfig, 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; primitives: Primitive[] }
|
||||
{ client: Client | null; primitives: Primitive[]; errorMsg: string | null }
|
||||
>();
|
||||
|
||||
// Whether initialized
|
||||
@@ -24,27 +25,76 @@ 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: {} };
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 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 reinitializeMcpClients() {
|
||||
logger.info("Reinitializing MCP clients...");
|
||||
// 遍历所有客户端,关闭
|
||||
try {
|
||||
for (const [clientId, clientData] of clientsMap.entries()) {
|
||||
clientData.client?.close();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to close clients: ${error}`);
|
||||
}
|
||||
// 清空状态
|
||||
clientsMap.clear();
|
||||
errorClients = [];
|
||||
initialized = false;
|
||||
// 重新初始化
|
||||
return initializeMcpClients();
|
||||
}
|
||||
|
||||
// Initialize all configured clients
|
||||
export async function initializeMcpClients() {
|
||||
// If already initialized, return
|
||||
if (initialized) {
|
||||
return;
|
||||
return { errorClients };
|
||||
}
|
||||
|
||||
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, config] of Object.entries(conf.mcpServers)) {
|
||||
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
|
||||
try {
|
||||
logger.info(`Initializing MCP client: ${clientId}`);
|
||||
const client = await createClient(config, clientId);
|
||||
const client = await createClient(serverConfig as ServerConfig, clientId);
|
||||
const primitives = await listPrimitives(client);
|
||||
clientsMap.set(clientId, { client, primitives });
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
@@ -58,8 +108,9 @@ export async function initializeMcpClients() {
|
||||
}
|
||||
|
||||
const availableClients = await getAvailableClients();
|
||||
|
||||
logger.info(`Available clients: ${availableClients.join(",")}`);
|
||||
|
||||
return { errorClients };
|
||||
}
|
||||
|
||||
// Execute MCP request
|
||||
@@ -87,9 +138,9 @@ export async function executeMcpAction(
|
||||
|
||||
// Get all available client IDs
|
||||
export async function getAvailableClients() {
|
||||
return Array.from(clientsMap.keys()).filter(
|
||||
(clientId) => !errorClients.includes(clientId),
|
||||
);
|
||||
return Array.from(clientsMap.entries())
|
||||
.filter(([_, data]) => data.errorMsg === null)
|
||||
.map(([clientId]) => clientId);
|
||||
}
|
||||
|
||||
// Get all primitives from all clients
|
||||
@@ -104,3 +155,62 @@ export async function getAllPrimitives(): Promise<
|
||||
primitives,
|
||||
}));
|
||||
}
|
||||
|
||||
// 获取客户端的 Primitives
|
||||
export async function getClientPrimitives(clientId: string) {
|
||||
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;
|
||||
} catch (error) {
|
||||
console.error(`Failed to get primitives for client ${clientId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 重启所有客户端
|
||||
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;
|
||||
}
|
||||
|
@@ -8,13 +8,29 @@
|
||||
"/Users/kadxy/Desktop"
|
||||
]
|
||||
},
|
||||
"everything": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-everything"]
|
||||
},
|
||||
"docker-mcp": {
|
||||
"command": "uvx",
|
||||
"args": ["docker-mcp"]
|
||||
},
|
||||
"difyworkflow": {
|
||||
"command": "mcp-difyworkflow-server",
|
||||
"args": ["-base-url", "23"],
|
||||
"env": {
|
||||
"DIFY_WORKFLOW_NAME": "23",
|
||||
"DIFY_API_KEYS": "23"
|
||||
}
|
||||
},
|
||||
"postgres": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "mcp/postgres", null]
|
||||
},
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@executeautomation/playwright-mcp-server"]
|
||||
},
|
||||
"gdrive": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-gdrive"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
206
app/mcp/preset-server.json
Normal file
206
app/mcp/preset-server.json
Normal file
@@ -0,0 +1,206 @@
|
||||
[
|
||||
{
|
||||
"id": "filesystem",
|
||||
"name": "Filesystem",
|
||||
"description": "Secure file operations with configurable access controls",
|
||||
"command": "npx",
|
||||
"baseArgs": ["-y", "@modelcontextprotocol/server-filesystem"],
|
||||
"configurable": true,
|
||||
"configSchema": {
|
||||
"properties": {
|
||||
"paths": {
|
||||
"type": "array",
|
||||
"description": "Allowed file system paths",
|
||||
"required": true,
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"argsMapping": {
|
||||
"paths": {
|
||||
"type": "spread",
|
||||
"position": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "github",
|
||||
"name": "GitHub",
|
||||
"description": "Repository management, file operations, and GitHub API integration",
|
||||
"command": "npx",
|
||||
"baseArgs": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"configurable": true,
|
||||
"configSchema": {
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "GitHub Personal Access Token",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"argsMapping": {
|
||||
"token": {
|
||||
"type": "env",
|
||||
"key": "GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gdrive",
|
||||
"name": "Google Drive",
|
||||
"description": "File access and search capabilities for Google Drive",
|
||||
"command": "npx",
|
||||
"baseArgs": ["-y", "@modelcontextprotocol/server-gdrive"],
|
||||
"configurable": false
|
||||
},
|
||||
{
|
||||
"id": "playwright",
|
||||
"name": "Playwright",
|
||||
"description": "Browser automation and webscrapping with Playwright",
|
||||
"command": "npx",
|
||||
"baseArgs": ["-y", "@executeautomation/playwright-mcp-server"],
|
||||
"configurable": false
|
||||
},
|
||||
{
|
||||
"id": "mongodb",
|
||||
"name": "MongoDB",
|
||||
"description": "Direct interaction with MongoDB databases",
|
||||
"command": "node",
|
||||
"baseArgs": ["dist/index.js"],
|
||||
"configurable": true,
|
||||
"configSchema": {
|
||||
"properties": {
|
||||
"connectionString": {
|
||||
"type": "string",
|
||||
"description": "MongoDB connection string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"argsMapping": {
|
||||
"connectionString": {
|
||||
"type": "single",
|
||||
"position": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "difyworkflow",
|
||||
"name": "Dify Workflow",
|
||||
"description": "Tools to query and execute Dify workflows",
|
||||
"command": "mcp-difyworkflow-server",
|
||||
"baseArgs": ["-base-url"],
|
||||
"configurable": true,
|
||||
"configSchema": {
|
||||
"properties": {
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"description": "Dify API base URL",
|
||||
"required": true
|
||||
},
|
||||
"workflowName": {
|
||||
"type": "string",
|
||||
"description": "Dify workflow name",
|
||||
"required": true
|
||||
},
|
||||
"apiKeys": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated Dify API keys",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"argsMapping": {
|
||||
"baseUrl": {
|
||||
"type": "single",
|
||||
"position": 1
|
||||
},
|
||||
"workflowName": {
|
||||
"type": "env",
|
||||
"key": "DIFY_WORKFLOW_NAME"
|
||||
},
|
||||
"apiKeys": {
|
||||
"type": "env",
|
||||
"key": "DIFY_API_KEYS"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "postgres",
|
||||
"name": "PostgreSQL",
|
||||
"description": "Read-only database access with schema inspection",
|
||||
"command": "docker",
|
||||
"baseArgs": ["run", "-i", "--rm", "mcp/postgres"],
|
||||
"configurable": true,
|
||||
"configSchema": {
|
||||
"properties": {
|
||||
"connectionString": {
|
||||
"type": "string",
|
||||
"description": "PostgreSQL connection string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"argsMapping": {
|
||||
"connectionString": {
|
||||
"type": "single",
|
||||
"position": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "brave-search",
|
||||
"name": "Brave Search",
|
||||
"description": "Web and local search using Brave's Search API",
|
||||
"command": "npx",
|
||||
"baseArgs": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||
"configurable": true,
|
||||
"configSchema": {
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"description": "Brave Search API Key",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"argsMapping": {
|
||||
"apiKey": {
|
||||
"type": "env",
|
||||
"key": "BRAVE_API_KEY"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "google-maps",
|
||||
"name": "Google Maps",
|
||||
"description": "Location services, directions, and place details",
|
||||
"command": "npx",
|
||||
"baseArgs": ["-y", "@modelcontextprotocol/server-google-maps"],
|
||||
"configurable": true,
|
||||
"configSchema": {
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"description": "Google Maps API Key",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"argsMapping": {
|
||||
"apiKey": {
|
||||
"type": "env",
|
||||
"key": "GOOGLE_MAPS_API_KEY"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "docker-mcp",
|
||||
"name": "Docker",
|
||||
"description": "Run and manage docker containers, docker compose, and logs",
|
||||
"command": "uvx",
|
||||
"baseArgs": ["docker-mcp"],
|
||||
"configurable": false
|
||||
}
|
||||
]
|
@@ -59,3 +59,41 @@ export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
|
||||
method: z.string(),
|
||||
params: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
// MCP 服务器配置相关类型
|
||||
export interface ServerConfig {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface McpConfig {
|
||||
mcpServers: Record<string, ServerConfig>;
|
||||
}
|
||||
|
||||
export interface ArgsMapping {
|
||||
type: "spread" | "single" | "env";
|
||||
position?: number;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface PresetServer {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
command: string;
|
||||
baseArgs: string[];
|
||||
configurable: boolean;
|
||||
configSchema?: {
|
||||
properties: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
minItems?: number;
|
||||
}
|
||||
>;
|
||||
};
|
||||
argsMapping?: Record<string, ArgsMapping>;
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
export function isMcpJson(content: string) {
|
||||
return content.match(/```json:mcp:(\w+)([\s\S]*?)```/);
|
||||
return content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
|
||||
}
|
||||
|
||||
export function extractMcpJson(content: string) {
|
||||
const match = content.match(/```json:mcp:(\w+)([\s\S]*?)```/);
|
||||
if (match) {
|
||||
const match = content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
|
||||
if (match && match.length === 3) {
|
||||
return { clientId: match[1], mcp: JSON.parse(match[2]) };
|
||||
}
|
||||
return null;
|
||||
|
Reference in New Issue
Block a user