Преглед на файлове

feat: Create all MCP Servers at startup

Kadxy преди 11 месеца
родител
ревизия
664879b9df
променени са 11 файла, в които са добавени 134 реда и са изтрити 165 реда
  1. 2 1
      .eslintignore
  2. 58 14
      app/mcp/actions.ts
  3. 5 8
      app/mcp/client.ts
  4. 6 67
      app/mcp/example.ts
  5. 17 12
      app/mcp/logger.ts
  6. 16 0
      app/mcp/mcp_config.json
  7. 0 40
      app/mcp/mcp_config.ts
  8. 3 2
      app/page.tsx
  9. 21 16
      app/store/chat.ts
  10. 2 1
      package.json
  11. 4 4
      yarn.lock

+ 2 - 1
.eslintignore

@@ -1 +1,2 @@
-public/serviceWorker.js
+public/serviceWorker.js
+app/mcp/mcp_config.json

+ 58 - 14
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<string, any>();
 
-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),
+  );
+}

+ 5 - 8
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());
 }

+ 6 - 67
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) => {

+ 17 - 12
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);
   }
 }

+ 16 - 0
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"]
+    }
+  }
+}

+ 0 - 40
app/mcp/mcp_config.ts

@@ -1,40 +0,0 @@
-export const MCP_CONF = {
-  "brave-search": {
-    command: "npx",
-    args: ["-y", "@modelcontextprotocol/server-brave-search"],
-    env: {
-      BRAVE_API_KEY: "<YOUR_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: "<YOUR_TOKEN>",
-    },
-  },
-  "google-maps": {
-    command: "npx",
-    args: ["-y", "@modelcontextprotocol/server-google-maps"],
-    env: {
-      GOOGLE_MAPS_API_KEY: "<YOUR_API_KEY>",
-    },
-  },
-  "aws-kb-retrieval": {
-    command: "npx",
-    args: ["-y", "@modelcontextprotocol/server-aws-kb-retrieval"],
-    env: {
-      AWS_ACCESS_KEY_ID: "<YOUR_ACCESS_KEY_HERE>",
-      AWS_SECRET_ACCESS_KEY: "<YOUR_SECRET_ACCESS_KEY_HERE>",
-      AWS_REGION: "<YOUR_AWS_REGION_HERE>",
-    },
-  },
-};

+ 3 - 2
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 (
     <>
       <Home />

+ 21 - 16
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);

+ 2 - 1
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",

+ 4 - 4
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"