From 664879b9df8c431664b06346962cff0319a3e85e Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sat, 28 Dec 2024 21:06:26 +0800 Subject: [PATCH] feat: Create all MCP Servers at startup --- .eslintignore | 3 +- app/mcp/actions.ts | 72 ++++++++++++++++++++++++++++++++-------- app/mcp/client.ts | 13 +++----- app/mcp/example.ts | 73 ++++------------------------------------- app/mcp/logger.ts | 29 +++++++++------- app/mcp/mcp_config.json | 16 +++++++++ app/mcp/mcp_config.ts | 40 ---------------------- app/page.tsx | 5 +-- app/store/chat.ts | 37 ++++++++++++--------- package.json | 3 +- yarn.lock | 8 ++--- 11 files changed, 134 insertions(+), 165 deletions(-) create mode 100644 app/mcp/mcp_config.json delete mode 100644 app/mcp/mcp_config.ts diff --git a/.eslintignore b/.eslintignore index 08975255475..8109e6bec48 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -public/serviceWorker.js \ No newline at end of file +public/serviceWorker.js +app/mcp/mcp_config.json \ No newline at end of file diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index 3d6ca4a68b8..af86834401b 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -2,32 +2,76 @@ import { createClient, executeRequest } from "./client"; import { MCPClientLogger } from "./logger"; -import { MCP_CONF } from "@/app/mcp/mcp_config"; +import conf from "./mcp_config.json"; const logger = new MCPClientLogger("MCP Server"); -let fsClient: any = null; +// Use Map to store all clients +const clientsMap = new Map(); -async function initFileSystemClient() { - if (!fsClient) { - fsClient = await createClient(MCP_CONF.filesystem, "fs"); - logger.success("FileSystem client initialized"); +// Whether initialized +let initialized = false; + +// Store failed clients +let errorClients: string[] = []; + +// Initialize all configured clients +export async function initializeMcpClients() { + // If already initialized, return + if (initialized) { + return; + } + + logger.info("Starting to initialize MCP clients..."); + + // Initialize all clients, key is clientId, value is client config + for (const [clientId, config] of Object.entries(conf.mcpServers)) { + try { + logger.info(`Initializing MCP client: ${clientId}`); + const client = await createClient(config, clientId); + clientsMap.set(clientId, client); + logger.success(`Client ${clientId} initialized`); + } catch (error) { + errorClients.push(clientId); + logger.error(`Failed to initialize client ${clientId}: ${error}`); + } } - return fsClient; -} -export async function executeMcpAction(request: any) { - "use server"; + 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(",")}`); +} + +// Execute MCP request +export async function executeMcpAction(clientId: string, request: any) { try { - if (!fsClient) { - await initFileSystemClient(); + // Find the corresponding client + const client = clientsMap.get(clientId); + if (!client) { + logger.error(`Client ${clientId} not found`); + return; } - logger.info("Executing MCP request for fs"); - return await executeRequest(fsClient, request); + logger.info(`Executing MCP request for ${clientId}`); + // Execute request and return result + return await executeRequest(client, request); } catch (error) { logger.error(`MCP execution error: ${error}`); throw error; } } + +// Get all available client IDs +export async function getAvailableClients() { + return Array.from(clientsMap.keys()).filter( + (clientId) => !errorClients.includes(clientId), + ); +} diff --git a/app/mcp/client.ts b/app/mcp/client.ts index d71314f3ac9..7eb55fb8222 100644 --- a/app/mcp/client.ts +++ b/app/mcp/client.ts @@ -29,11 +29,9 @@ export async function createClient( }, { capabilities: { - roots: { - // listChanged indicates whether the client will emit notifications when the list of roots changes. - // listChanged 指示客户端在根列表更改时是否发出通知。 - listChanged: true, - }, + // roots: { + // listChanged: true, + // }, }, }, ); @@ -80,8 +78,7 @@ export async function listPrimitives(client: Client) { return primitives; } +/** Execute a request */ export async function executeRequest(client: Client, request: any) { - const r = client.request(request, z.any()); - console.log(r); - return r; + return client.request(request, z.any()); } diff --git a/app/mcp/example.ts b/app/mcp/example.ts index d924ba66470..83fc8784cf6 100644 --- a/app/mcp/example.ts +++ b/app/mcp/example.ts @@ -1,35 +1,16 @@ import { createClient, listPrimitives } from "@/app/mcp/client"; import { MCPClientLogger } from "@/app/mcp/logger"; -import { z } from "zod"; -import { MCP_CONF } from "@/app/mcp/mcp_config"; +import conf from "./mcp_config.json"; -const logger = new MCPClientLogger("MCP FS Example", true); - -const ListAllowedDirectoriesResultSchema = z.object({ - content: z.array( - z.object({ - type: z.string(), - text: z.string(), - }), - ), -}); - -const ReadFileResultSchema = z.object({ - content: z.array( - z.object({ - type: z.string(), - text: z.string(), - }), - ), -}); +const logger = new MCPClientLogger("MCP Server Example", true); async function main() { logger.info("Connecting to server..."); - const client = await createClient(MCP_CONF.filesystem, "fs"); + const client = await createClient(conf.mcpServers.everything, "everything"); const primitives = await listPrimitives(client); - logger.success(`Connected to server fs`); + logger.success(`Connected to server everything`); logger.info( `server capabilities: ${Object.keys( @@ -37,53 +18,11 @@ async function main() { ).join(", ")}`, ); - logger.debug("Server supports the following primitives:"); + logger.info("Server supports the following primitives:"); primitives.forEach((primitive) => { - logger.debug("\n" + JSON.stringify(primitive, null, 2)); + logger.info("\n" + JSON.stringify(primitive, null, 2)); }); - - const listAllowedDirectories = async () => { - const result = await client.request( - { - method: "tools/call", - params: { - name: "list_allowed_directories", - arguments: {}, - }, - }, - ListAllowedDirectoriesResultSchema, - ); - logger.success(`Allowed directories: ${result.content[0].text}`); - return result; - }; - - const readFile = async (path: string) => { - const result = await client.request( - { - method: "tools/call", - params: { - name: "read_file", - arguments: { - path: path, - }, - }, - }, - ReadFileResultSchema, - ); - logger.success(`File contents for ${path}:\n${result.content[0].text}`); - return result; - }; - - try { - logger.info("Example 1: List allowed directories\n"); - await listAllowedDirectories(); - - logger.info("\nExample 2: Read a file\n"); - await readFile("/users/kadxy/desktop/test.txt"); - } catch (error) { - logger.error(`Error executing examples: ${error}`); - } } main().catch((error) => { diff --git a/app/mcp/logger.ts b/app/mcp/logger.ts index a39304afe91..25129c592c3 100644 --- a/app/mcp/logger.ts +++ b/app/mcp/logger.ts @@ -1,3 +1,4 @@ +// ANSI color codes for terminal output const colors = { reset: "\x1b[0m", bright: "\x1b[1m", @@ -21,40 +22,44 @@ export class MCPClientLogger { } info(message: any) { - this.log(colors.blue, message); + this.print(colors.blue, message); } success(message: any) { - this.log(colors.green, message); + this.print(colors.green, message); } error(message: any) { - const formattedMessage = this.formatMessage(message); - console.error( - `${colors.red}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`, - ); + this.print(colors.red, message); } warn(message: any) { - this.log(colors.yellow, message); + this.print(colors.yellow, message); } debug(message: any) { if (this.debugMode) { - this.log(colors.dim, message); + this.print(colors.dim, message); } } + /** + * Format message to string, if message is object, convert to JSON string + */ private formatMessage(message: any): string { return typeof message === "object" ? JSON.stringify(message, null, 2) : message; } - private log(color: string, message: any) { + /** + * Print formatted message to console + */ + private print(color: string, message: any) { const formattedMessage = this.formatMessage(message); - console.log( - `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`, - ); + const logMessage = `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`; + + // 只使用 console.log,这样日志会显示在 Tauri 的终端中 + console.log(logMessage); } } diff --git a/app/mcp/mcp_config.json b/app/mcp/mcp_config.json new file mode 100644 index 00000000000..6ad18236b52 --- /dev/null +++ b/app/mcp/mcp_config.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/Users/kadxy/Desktop" + ] + }, + "everything": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-everything"] + } + } +} diff --git a/app/mcp/mcp_config.ts b/app/mcp/mcp_config.ts deleted file mode 100644 index 044d04052a1..00000000000 --- a/app/mcp/mcp_config.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const MCP_CONF = { - "brave-search": { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-brave-search"], - env: { - BRAVE_API_KEY: "", - }, - }, - filesystem: { - command: "npx", - args: [ - "-y", - "@modelcontextprotocol/server-filesystem", - "/Users/kadxy/Desktop", - ], - }, - github: { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-github"], - env: { - GITHUB_PERSONAL_ACCESS_TOKEN: "", - }, - }, - "google-maps": { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-google-maps"], - env: { - GOOGLE_MAPS_API_KEY: "", - }, - }, - "aws-kb-retrieval": { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-aws-kb-retrieval"], - env: { - AWS_ACCESS_KEY_ID: "", - AWS_SECRET_ACCESS_KEY: "", - AWS_REGION: "", - }, - }, -}; diff --git a/app/page.tsx b/app/page.tsx index b3f169a9b74..d4ba2a27613 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,13 @@ import { Analytics } from "@vercel/analytics/react"; - import { Home } from "./components/home"; - import { getServerSideConfig } from "./config/server"; +import { initializeMcpClients } from "./mcp/actions"; const serverConfig = getServerSideConfig(); export default async function App() { + await initializeMcpClients(); + return ( <> diff --git a/app/store/chat.ts b/app/store/chat.ts index 27d1f8620a3..3444bb43635 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -356,6 +356,27 @@ export const useChatStore = createPersistStore( onNewMessage(message: ChatMessage, targetSession: ChatSession) { get().updateTargetSession(targetSession, (session) => { + // Check and process MCP JSON + const content = + typeof message.content === "string" ? message.content : ""; + const mcpMatch = content.match(/```json:mcp:(\w+)([\s\S]*?)```/); + if (mcpMatch) { + try { + const clientId = mcpMatch[1]; + const mcp = JSON.parse(mcpMatch[2]); + console.log("[MCP Request]", clientId, mcp); + // Execute MCP action + executeMcpAction(clientId, mcp) + .then((result) => { + console.log("[MCP Response]", result); + }) + .catch((error) => { + console.error("[MCP Error]", error); + }); + } catch (error) { + console.error("[MCP Error]", error); + } + } session.messages = session.messages.concat(); session.lastUpdate = Date.now(); }); @@ -429,22 +450,6 @@ export const useChatStore = createPersistStore( async onFinish(message) { botMessage.streaming = false; if (message) { - // console.log("[Bot Response] ", message); - const mcpMatch = message.match(/```json:mcp([\s\S]*?)```/); - if (mcpMatch) { - try { - const mcp = JSON.parse(mcpMatch[1]); - console.log("[MCP Request]", mcp); - - // 直接调用服务器端 action - const result = await executeMcpAction(mcp); - console.log("[MCP Response]", result); - } catch (error) { - console.error("[MCP Error]", error); - } - } else { - console.log("[MCP] No MCP found in response"); - } botMessage.content = message; botMessage.date = new Date().toLocaleString(); get().onNewMessage(botMessage, session); diff --git a/package.json b/package.json index a17f8ffa9cc..0efe27b391a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"", "app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"", "app:build": "yarn mask && yarn tauri build", + "app:clear": "yarn tauri dev", "prompts": "node ./scripts/fetch-prompts.mjs", "prepare": "husky install", "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev", @@ -58,7 +59,7 @@ "zustand": "^4.3.8" }, "devDependencies": { - "@tauri-apps/api": "^1.6.0", + "@tauri-apps/api": "^2.1.1", "@tauri-apps/cli": "1.5.11", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/yarn.lock b/yarn.lock index 138f3c8519b..5b9741b2b4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2038,10 +2038,10 @@ dependencies: tslib "^2.4.0" -"@tauri-apps/api@^1.6.0": - version "1.6.0" - resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz#745b7e4e26782c3b2ad9510d558fa5bb2cf29186" - integrity sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg== +"@tauri-apps/api@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.1.1.tgz#77d4ddb683d31072de4e6a47c8613d9db011652b" + integrity sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A== "@tauri-apps/cli-darwin-arm64@1.5.11": version "1.5.11"