From e1c7c54dfaf82c37450d0ed3a124f8598bc0249b Mon Sep 17 00:00:00 2001 From: river Date: Mon, 23 Dec 2024 22:32:36 +0800 Subject: [PATCH 01/29] chore: change md --- README_CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_CN.md b/README_CN.md index 8173b9c4d1c..31b596f0bbd 100644 --- a/README_CN.md +++ b/README_CN.md @@ -6,7 +6,7 @@

NextChat

-一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。 +一键免费部署你的私人 ChatGPT 网页应用,支持 Claude, GPT4 & Gemini Pro 模型。 [NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) From c3108ad333419ecb0d16a031d4f4603f0f781832 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sat, 28 Dec 2024 14:31:43 +0800 Subject: [PATCH 02/29] feat: simple MCP example --- app/mcp/actions.ts | 33 ++++++++++++++++ app/mcp/client.ts | 87 ++++++++++++++++++++++++++++++++++++++++ app/mcp/example.ts | 92 +++++++++++++++++++++++++++++++++++++++++++ app/mcp/logger.ts | 60 ++++++++++++++++++++++++++++ app/mcp/mcp_config.ts | 40 +++++++++++++++++++ app/store/chat.ts | 19 ++++++++- next.config.mjs | 9 +++-- package.json | 6 ++- tsconfig.json | 4 +- yarn.lock | 72 ++++++++++++++++++++++++++++++++- 10 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 app/mcp/actions.ts create mode 100644 app/mcp/client.ts create mode 100644 app/mcp/example.ts create mode 100644 app/mcp/logger.ts create mode 100644 app/mcp/mcp_config.ts diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts new file mode 100644 index 00000000000..3d6ca4a68b8 --- /dev/null +++ b/app/mcp/actions.ts @@ -0,0 +1,33 @@ +"use server"; + +import { createClient, executeRequest } from "./client"; +import { MCPClientLogger } from "./logger"; +import { MCP_CONF } from "@/app/mcp/mcp_config"; + +const logger = new MCPClientLogger("MCP Server"); + +let fsClient: any = null; + +async function initFileSystemClient() { + if (!fsClient) { + fsClient = await createClient(MCP_CONF.filesystem, "fs"); + logger.success("FileSystem client initialized"); + } + return fsClient; +} + +export async function executeMcpAction(request: any) { + "use server"; + + try { + if (!fsClient) { + await initFileSystemClient(); + } + + logger.info("Executing MCP request for fs"); + return await executeRequest(fsClient, request); + } catch (error) { + logger.error(`MCP execution error: ${error}`); + throw error; + } +} diff --git a/app/mcp/client.ts b/app/mcp/client.ts new file mode 100644 index 00000000000..d71314f3ac9 --- /dev/null +++ b/app/mcp/client.ts @@ -0,0 +1,87 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { MCPClientLogger } from "./logger"; +import { z } from "zod"; + +export interface ServerConfig { + command: string; + args?: string[]; + env?: Record; +} + +const logger = new MCPClientLogger(); + +export async function createClient( + serverConfig: ServerConfig, + name: string, +): Promise { + logger.info(`Creating client for server ${name}`); + + const transport = new StdioClientTransport({ + command: serverConfig.command, + args: serverConfig.args, + env: serverConfig.env, + }); + const client = new Client( + { + name: `nextchat-mcp-client-${name}`, + version: "1.0.0", + }, + { + capabilities: { + roots: { + // listChanged indicates whether the client will emit notifications when the list of roots changes. + // listChanged 指示客户端在根列表更改时是否发出通知。 + listChanged: true, + }, + }, + }, + ); + await client.connect(transport); + return client; +} + +interface Primitive { + type: "resource" | "tool" | "prompt"; + value: any; +} + +/** List all resources, tools, and prompts */ +export async function listPrimitives(client: Client) { + 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 executeRequest(client: Client, request: any) { + const r = client.request(request, z.any()); + console.log(r); + return r; +} diff --git a/app/mcp/example.ts b/app/mcp/example.ts new file mode 100644 index 00000000000..d924ba66470 --- /dev/null +++ b/app/mcp/example.ts @@ -0,0 +1,92 @@ +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"; + +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(), + }), + ), +}); + +async function main() { + logger.info("Connecting to server..."); + + const client = await createClient(MCP_CONF.filesystem, "fs"); + const primitives = await listPrimitives(client); + + logger.success(`Connected to server fs`); + + logger.info( + `server capabilities: ${Object.keys( + client.getServerCapabilities() ?? [], + ).join(", ")}`, + ); + + logger.debug("Server supports the following primitives:"); + + primitives.forEach((primitive) => { + logger.debug("\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) => { + logger.error(error); + process.exit(1); +}); diff --git a/app/mcp/logger.ts b/app/mcp/logger.ts new file mode 100644 index 00000000000..a39304afe91 --- /dev/null +++ b/app/mcp/logger.ts @@ -0,0 +1,60 @@ +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + dim: "\x1b[2m", + green: "\x1b[32m", + yellow: "\x1b[33m", + red: "\x1b[31m", + blue: "\x1b[34m", +}; + +export class MCPClientLogger { + private readonly prefix: string; + private readonly debugMode: boolean; + + constructor( + prefix: string = "NextChat MCP Client", + debugMode: boolean = false, + ) { + this.prefix = prefix; + this.debugMode = debugMode; + } + + info(message: any) { + this.log(colors.blue, message); + } + + success(message: any) { + this.log(colors.green, message); + } + + error(message: any) { + const formattedMessage = this.formatMessage(message); + console.error( + `${colors.red}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`, + ); + } + + warn(message: any) { + this.log(colors.yellow, message); + } + + debug(message: any) { + if (this.debugMode) { + this.log(colors.dim, message); + } + } + + private formatMessage(message: any): string { + return typeof message === "object" + ? JSON.stringify(message, null, 2) + : message; + } + + private log(color: string, message: any) { + const formattedMessage = this.formatMessage(message); + console.log( + `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`, + ); + } +} diff --git a/app/mcp/mcp_config.ts b/app/mcp/mcp_config.ts new file mode 100644 index 00000000000..044d04052a1 --- /dev/null +++ b/app/mcp/mcp_config.ts @@ -0,0 +1,40 @@ +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/store/chat.ts b/app/store/chat.ts index 63d7394ece6..27d1f8620a3 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -29,6 +29,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config"; import { useAccessStore } from "./access"; import { collectModelsWithDefaultModel } from "../utils/model"; import { createEmptyMask, Mask } from "./mask"; +import { executeMcpAction } from "../mcp/actions"; const localStorage = safeLocalStorage(); @@ -425,9 +426,25 @@ export const useChatStore = createPersistStore( session.messages = session.messages.concat(); }); }, - onFinish(message) { + 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/next.config.mjs b/next.config.mjs index 2bb6bc4f4b2..80241913929 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -32,6 +32,7 @@ const nextConfig = { }, experimental: { forceSwcTransforms: true, + serverActions: true, }, }; @@ -71,8 +72,10 @@ if (mode !== "export") { // }, { // https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions - source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*", - destination: "https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*", + source: + "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*", + destination: + "https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*", }, { source: "/api/proxy/google/:path*", @@ -99,7 +102,7 @@ if (mode !== "export") { destination: "https://dashscope.aliyuncs.com/api/:path*", }, ]; - + return { beforeFiles: ret, }; diff --git a/package.json b/package.json index e081567a4b1..a17f8ffa9cc 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@fortaine/fetch-event-source": "^3.0.6", "@hello-pangea/dnd": "^16.5.0", + "@modelcontextprotocol/sdk": "^1.0.4", "@next/third-parties": "^14.1.0", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", @@ -49,11 +50,12 @@ "remark-breaks": "^3.0.2", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", + "rt-client": "https://github.com/Azure-Samples/aoai-realtime-audio-sdk/releases/download/js/v0.5.0/rt-client-0.5.0.tgz", "sass": "^1.59.2", "spark-md5": "^3.0.2", "use-debounce": "^9.0.4", - "zustand": "^4.3.8", - "rt-client": "https://github.com/Azure-Samples/aoai-realtime-audio-sdk/releases/download/js/v0.5.0/rt-client-0.5.0.tgz" + "zod": "^3.24.1", + "zustand": "^4.3.8" }, "devDependencies": { "@tauri-apps/api": "^1.6.0", diff --git a/tsconfig.json b/tsconfig.json index c73eef3e876..6d24b42f1de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2015", + "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, @@ -23,6 +23,6 @@ "@/*": ["./*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index dffc35e9cb7..138f3c8519b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1797,6 +1797,15 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@modelcontextprotocol/sdk@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.0.4.tgz#34ad1edd3db7dd7154e782312dfb29d2d0c11d21" + integrity sha512-C+jw1lF6HSGzs7EZpzHbXfzz9rj9him4BaoumlTciW/IDDgIpweF/qiCWKlP02QKg5PPcgY6xY2WCt5y2tpYow== + dependencies: + content-type "^1.0.5" + raw-body "^3.0.0" + zod "^3.23.8" + "@next/env@14.1.1": version "14.1.1" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.1.tgz#80150a8440eb0022a73ba353c6088d419b908bac" @@ -3039,6 +3048,11 @@ busboy@1.6.0: dependencies: streamsearch "^1.1.0" +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -3285,6 +3299,11 @@ concurrently@^8.2.2: tree-kill "^1.2.2" yargs "^17.7.2" +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -3849,6 +3868,11 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + dequal@^2.0.0, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -5007,6 +5031,17 @@ html-to-image@^1.11.11: resolved "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.11.tgz#c0f8a34dc9e4b97b93ff7ea286eb8562642ebbea" integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA== +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -5095,7 +5130,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7138,6 +7173,16 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -7569,6 +7614,11 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -7699,6 +7749,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" @@ -7977,6 +8032,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + tough-cookie@^4.1.2: version "4.1.4" resolved "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" @@ -8219,6 +8279,11 @@ universalify@^0.2.0: resolved "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + update-browserslist-db@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" @@ -8572,6 +8637,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod@^3.23.8, zod@^3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== + zustand@^4.3.8: version "4.3.8" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.8.tgz#37113df8e9e1421b0be1b2dca02b49b76210e7c4" 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 03/29] 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" From e1ba8f1b0f122a73194b2f3716fdb78173647e05 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sun, 29 Dec 2024 08:29:02 +0800 Subject: [PATCH 04/29] feat: Send MCP response as a user --- app/mcp/utils.ts | 11 ++++++++++ app/store/chat.ts | 52 ++++++++++++++++++++++++++++------------------- 2 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 app/mcp/utils.ts diff --git a/app/mcp/utils.ts b/app/mcp/utils.ts new file mode 100644 index 00000000000..5b6dcbf027f --- /dev/null +++ b/app/mcp/utils.ts @@ -0,0 +1,11 @@ +export function isMcpJson(content: string) { + return content.match(/```json:mcp:(\w+)([\s\S]*?)```/); +} + +export function extractMcpJson(content: string) { + const match = content.match(/```json:mcp:(\w+)([\s\S]*?)```/); + if (match) { + return { clientId: match[1], mcp: JSON.parse(match[2]) }; + } + return null; +} diff --git a/app/store/chat.ts b/app/store/chat.ts index 3444bb43635..d30fa1fea48 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -30,6 +30,7 @@ import { useAccessStore } from "./access"; import { collectModelsWithDefaultModel } from "../utils/model"; import { createEmptyMask, Mask } from "./mask"; import { executeMcpAction } from "../mcp/actions"; +import { extractMcpJson, isMcpJson } from "../mcp/utils"; const localStorage = safeLocalStorage(); @@ -356,31 +357,14 @@ 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(); }); + get().updateStat(message, targetSession); + + get().checkMcpJson(message); + get().summarizeSession(false, targetSession); }, @@ -786,6 +770,32 @@ export const useChatStore = createPersistStore( lastInput, }); }, + checkMcpJson(message: ChatMessage) { + const content = + typeof message.content === "string" ? message.content : ""; + if (isMcpJson(content)) { + try { + const mcpRequest = extractMcpJson(content); + if (mcpRequest) { + console.debug("[MCP Request]", mcpRequest); + + executeMcpAction(mcpRequest.clientId, mcpRequest.mcp) + .then((result) => { + console.log("[MCP Response]", result); + // 直接使用onUserInput发送结果 + get().onUserInput( + typeof result === "object" + ? JSON.stringify(result) + : String(result), + ); + }) + .catch((error) => showToast(String(error))); + } + } catch (error) { + console.error("[MCP Error]", error); + } + } + }, }; return methods; From fe67f79050c7f4b8971f9b9aabc22c5fd23bac07 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sun, 29 Dec 2024 09:24:52 +0800 Subject: [PATCH 05/29] feat: MCP message type --- app/mcp/actions.ts | 9 +++++-- app/mcp/client.ts | 6 ++++- app/mcp/types.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++++ app/store/chat.ts | 48 ++++++++++++++++++++++-------------- 4 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 app/mcp/types.ts diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index af86834401b..5fe611b3a84 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -3,8 +3,9 @@ import { createClient, executeRequest } from "./client"; import { MCPClientLogger } from "./logger"; import conf from "./mcp_config.json"; +import { McpRequestMessage } from "./types"; -const logger = new MCPClientLogger("MCP Server"); +const logger = new MCPClientLogger("MCP Actions"); // Use Map to store all clients const clientsMap = new Map(); @@ -51,7 +52,10 @@ export async function initializeMcpClients() { } // Execute MCP request -export async function executeMcpAction(clientId: string, request: any) { +export async function executeMcpAction( + clientId: string, + request: McpRequestMessage, +) { try { // Find the corresponding client const client = clientsMap.get(clientId); @@ -61,6 +65,7 @@ export async function executeMcpAction(clientId: string, request: any) { } logger.info(`Executing MCP request for ${clientId}`); + // Execute request and return result return await executeRequest(client, request); } catch (error) { diff --git a/app/mcp/client.ts b/app/mcp/client.ts index 7eb55fb8222..0600f00be92 100644 --- a/app/mcp/client.ts +++ b/app/mcp/client.ts @@ -1,6 +1,7 @@ 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 { z } from "zod"; export interface ServerConfig { @@ -79,6 +80,9 @@ export async function listPrimitives(client: Client) { } /** Execute a request */ -export async function executeRequest(client: Client, request: any) { +export async function executeRequest( + client: Client, + request: McpRequestMessage, +) { return client.request(request, z.any()); } diff --git a/app/mcp/types.ts b/app/mcp/types.ts new file mode 100644 index 00000000000..763121bad88 --- /dev/null +++ b/app/mcp/types.ts @@ -0,0 +1,61 @@ +// ref: https://spec.modelcontextprotocol.io/specification/basic/messages/ + +import { z } from "zod"; + +export interface McpRequestMessage { + jsonrpc?: "2.0"; + id?: string | number; + method: "tools/call" | string; + params?: { + [key: string]: unknown; + }; +} + +export const McpRequestMessageSchema: z.ZodType = z.object({ + jsonrpc: z.literal("2.0").optional(), + id: z.union([z.string(), z.number()]).optional(), + method: z.string(), + params: z.record(z.unknown()).optional(), +}); + +export interface McpResponseMessage { + jsonrpc?: "2.0"; + id?: string | number; + result?: { + [key: string]: unknown; + }; + error?: { + code: number; + message: string; + data?: unknown; + }; +} + +export const McpResponseMessageSchema: z.ZodType = z.object( + { + jsonrpc: z.literal("2.0").optional(), + id: z.union([z.string(), z.number()]).optional(), + result: z.record(z.unknown()).optional(), + error: z + .object({ + code: z.number(), + message: z.string(), + data: z.unknown().optional(), + }) + .optional(), + }, +); + +export interface McpNotifications { + jsonrpc?: "2.0"; + method: string; + params?: { + [key: string]: unknown; + }; +} + +export const McpNotificationsSchema: z.ZodType = z.object({ + jsonrpc: z.literal("2.0").optional(), + method: z.string(), + params: z.record(z.unknown()).optional(), +}); diff --git a/app/store/chat.ts b/app/store/chat.ts index d30fa1fea48..e0ee956219c 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -1,4 +1,9 @@ -import { getMessageTextContent, trimTopic } from "../utils"; +import { + getMessageTextContent, + isDalle3, + safeLocalStorage, + trimTopic, +} from "../utils"; import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; import { nanoid } from "nanoid"; @@ -14,14 +19,13 @@ import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, DEFAULT_SYSTEM_TEMPLATE, + GEMINI_SUMMARIZE_MODEL, KnowledgeCutOffDate, + ServiceProvider, StoreKey, SUMMARIZE_MODEL, - GEMINI_SUMMARIZE_MODEL, - ServiceProvider, } from "../constant"; import Locale, { getLang } from "../locales"; -import { isDalle3, safeLocalStorage } from "../utils"; import { prettyObject } from "../utils/format"; import { createPersistStore } from "../utils/store"; import { estimateTokenLength } from "../utils/token"; @@ -55,6 +59,7 @@ export type ChatMessage = RequestMessage & { model?: ModelType; tools?: ChatMessageTool[]; audio_url?: string; + isMcpResponse?: boolean; }; export function createMessage(override: Partial): ChatMessage { @@ -368,20 +373,22 @@ export const useChatStore = createPersistStore( get().summarizeSession(false, targetSession); }, - async onUserInput(content: string, attachImages?: string[]) { + async onUserInput( + content: string, + attachImages?: string[], + isMcpResponse?: boolean, + ) { const session = get().currentSession(); const modelConfig = session.mask.modelConfig; - const userContent = fillTemplateWith(content, modelConfig); - console.log("[User Input] after template: ", userContent); - - let mContent: string | MultimodalContent[] = userContent; + // MCP Response no need to fill template + let mContent: string | MultimodalContent[] = isMcpResponse + ? content + : fillTemplateWith(content, modelConfig); - if (attachImages && attachImages.length > 0) { + if (!isMcpResponse && attachImages && attachImages.length > 0) { mContent = [ - ...(userContent - ? [{ type: "text" as const, text: userContent }] - : []), + ...(content ? [{ type: "text" as const, text: content }] : []), ...attachImages.map((url) => ({ type: "image_url" as const, image_url: { url }, @@ -392,6 +399,7 @@ export const useChatStore = createPersistStore( let userMessage: ChatMessage = createMessage({ role: "user", content: mContent, + isMcpResponse, }); const botMessage: ChatMessage = createMessage({ @@ -770,9 +778,10 @@ export const useChatStore = createPersistStore( lastInput, }); }, + + /** check if the message contains MCP JSON and execute the MCP action */ checkMcpJson(message: ChatMessage) { - const content = - typeof message.content === "string" ? message.content : ""; + const content = getMessageTextContent(message); if (isMcpJson(content)) { try { const mcpRequest = extractMcpJson(content); @@ -782,11 +791,14 @@ export const useChatStore = createPersistStore( executeMcpAction(mcpRequest.clientId, mcpRequest.mcp) .then((result) => { console.log("[MCP Response]", result); - // 直接使用onUserInput发送结果 - get().onUserInput( + const mcpResponse = typeof result === "object" ? JSON.stringify(result) - : String(result), + : String(result); + get().onUserInput( + `\`\`\`json:mcp:${mcpRequest.clientId}\n${mcpResponse}\n\`\`\``, + [], + true, ); }) .catch((error) => showToast(String(error))); From 77be190d763189915c520d431fc4aa889ca96c7e Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 9 Jan 2025 10:09:46 +0800 Subject: [PATCH 06/29] feat: carry mcp primitives content as a system prompt --- app/components/chat.tsx | 497 +++++++++++++++++++++------------------- app/constant.ts | 106 +++++++++ app/mcp/actions.ts | 36 ++- app/mcp/client.ts | 4 +- app/mcp/example.ts | 24 +- app/store/chat.ts | 48 +++- 6 files changed, 444 insertions(+), 271 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 51fe74fe7be..75120041ce6 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1,17 +1,18 @@ import { useDebouncedCallback } from "use-debounce"; import React, { - useState, - useRef, - useEffect, - useMemo, - useCallback, Fragment, RefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, } from "react"; import SendWhiteIcon from "../icons/send-white.svg"; import BrainIcon from "../icons/brain.svg"; import RenameIcon from "../icons/rename.svg"; +import EditIcon from "../icons/rename.svg"; import ExportIcon from "../icons/share.svg"; import ReturnIcon from "../icons/return.svg"; import CopyIcon from "../icons/copy.svg"; @@ -24,11 +25,11 @@ import MaskIcon from "../icons/mask.svg"; import MaxIcon from "../icons/max.svg"; import MinIcon from "../icons/min.svg"; import ResetIcon from "../icons/reload.svg"; +import ReloadIcon from "../icons/reload.svg"; import BreakIcon from "../icons/break.svg"; import SettingsIcon from "../icons/chat-settings.svg"; import DeleteIcon from "../icons/clear.svg"; import PinIcon from "../icons/pin.svg"; -import EditIcon from "../icons/rename.svg"; import ConfirmIcon from "../icons/confirm.svg"; import CloseIcon from "../icons/close.svg"; import CancelIcon from "../icons/cancel.svg"; @@ -45,33 +46,32 @@ import QualityIcon from "../icons/hd.svg"; import StyleIcon from "../icons/palette.svg"; import PluginIcon from "../icons/plugin.svg"; import ShortcutkeyIcon from "../icons/shortcutkey.svg"; -import ReloadIcon from "../icons/reload.svg"; import HeadphoneIcon from "../icons/headphone.svg"; import { - ChatMessage, - SubmitKey, - useChatStore, BOT_HELLO, + ChatMessage, createMessage, - useAccessStore, - Theme, - useAppConfig, DEFAULT_TOPIC, ModelType, + SubmitKey, + Theme, + useAccessStore, + useAppConfig, + useChatStore, usePluginStore, } from "../store"; import { - copyToClipboard, - selectOrCopy, autoGrowTextArea, - useMobileScreen, - getMessageTextContent, + copyToClipboard, getMessageImages, - isVisionModel, + getMessageTextContent, isDalle3, - showPlugins, + isVisionModel, safeLocalStorage, + selectOrCopy, + showPlugins, + useMobileScreen, } from "../utils"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; @@ -79,7 +79,7 @@ import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; import dynamic from "next/dynamic"; import { ChatControllerPool } from "../client/controller"; -import { DalleSize, DalleQuality, DalleStyle } from "../typing"; +import { DalleQuality, DalleSize, DalleStyle } from "../typing"; import { Prompt, usePromptStore } from "../store/prompt"; import Locale from "../locales"; @@ -102,8 +102,8 @@ import { ModelProvider, Path, REQUEST_TIMEOUT_MS, - UNFINISHED_INPUT, ServiceProvider, + UNFINISHED_INPUT, } from "../constant"; import { Avatar } from "./emoji"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; @@ -113,9 +113,7 @@ import { prettyObject } from "../utils/format"; import { ExportMessageModal } from "./exporter"; import { getClientConfig } from "../config/client"; import { useAllModels } from "../utils/hooks"; -import { MultimodalContent } from "../client/api"; - -import { ClientApi } from "../client/api"; +import { ClientApi, MultimodalContent } from "../client/api"; import { createTTSPlayer } from "../utils/audio"; import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts"; @@ -427,6 +425,7 @@ function useScrollToBottom( // for auto-scroll const [autoScroll, setAutoScroll] = useState(true); + function scrollDomToBottom() { const dom = scrollRef.current; if (dom) { @@ -473,6 +472,7 @@ export function ChatActions(props: { // switch themes const theme = config.theme; + function nextTheme() { const themes = [Theme.Auto, Theme.Light, Theme.Dark]; const themeIndex = themes.indexOf(theme); @@ -1237,6 +1237,7 @@ function _Chat() { const accessStore = useAccessStore(); const [speechStatus, setSpeechStatus] = useState(false); const [speechLoading, setSpeechLoading] = useState(false); + async function openaiSpeech(text: string) { if (speechStatus) { ttsPlayer.stop(); @@ -1336,6 +1337,7 @@ function _Chat() { const [msgRenderIndex, _setMsgRenderIndex] = useState( Math.max(0, renderMessages.length - CHAT_PAGE_SIZE), ); + function setMsgRenderIndex(newIndex: number) { newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex); newIndex = Math.max(0, newIndex); @@ -1371,6 +1373,7 @@ function _Chat() { setHitBottom(isHitBottom); setAutoScroll(isHitBottom); }; + function scrollToBottom() { setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE); scrollDomToBottom(); @@ -1712,252 +1715,264 @@ function _Chat() { setAutoScroll(false); }} > - {messages.map((message, i) => { - const isUser = message.role === "user"; - const isContext = i < context.length; - const showActions = - i > 0 && - !(message.preview || message.content.length === 0) && - !isContext; - const showTyping = message.preview || message.streaming; - - const shouldShowClearContextDivider = - i === clearContextIndex - 1; - - return ( - -
-
-
-
-
- } - aria={Locale.Chat.Actions.Edit} - onClick={async () => { - const newMessage = await showPrompt( - Locale.Chat.Actions.Edit, - getMessageTextContent(message), - 10, - ); - let newContent: string | MultimodalContent[] = - newMessage; - const images = getMessageImages(message); - if (images.length > 0) { - newContent = [ - { type: "text", text: newMessage }, - ]; - for (let i = 0; i < images.length; i++) { - newContent.push({ - type: "image_url", - image_url: { - url: images[i], - }, - }); + {messages + // TODO + // .filter((m) => !m.isMcpResponse) + .map((message, i) => { + const isUser = message.role === "user"; + const isContext = i < context.length; + const showActions = + i > 0 && + !(message.preview || message.content.length === 0) && + !isContext; + const showTyping = message.preview || message.streaming; + + const shouldShowClearContextDivider = + i === clearContextIndex - 1; + + return ( + +
+
+
+
+
+ } + aria={Locale.Chat.Actions.Edit} + onClick={async () => { + const newMessage = await showPrompt( + Locale.Chat.Actions.Edit, + getMessageTextContent(message), + 10, + ); + let newContent: + | string + | MultimodalContent[] = newMessage; + const images = getMessageImages(message); + if (images.length > 0) { + newContent = [ + { type: "text", text: newMessage }, + ]; + for (let i = 0; i < images.length; i++) { + newContent.push({ + type: "image_url", + image_url: { + url: images[i], + }, + }); + } } - } - chatStore.updateTargetSession( - session, - (session) => { - const m = session.mask.context - .concat(session.messages) - .find((m) => m.id === message.id); - if (m) { - m.content = newContent; + chatStore.updateTargetSession( + session, + (session) => { + const m = session.mask.context + .concat(session.messages) + .find((m) => m.id === message.id); + if (m) { + m.content = newContent; + } + }, + ); + }} + > +
+ {isUser ? ( + + ) : ( + <> + {["system"].includes(message.role) ? ( + + ) : ( + + /> + )} + + )}
- {isUser ? ( - - ) : ( - <> - {["system"].includes(message.role) ? ( - - ) : ( - - )} - + {!isUser && ( +
+ {message.model} +
)} -
- {!isUser && ( -
- {message.model} -
- )} - {showActions && ( -
-
- {message.streaming ? ( - } - onClick={() => onUserStop(message.id ?? i)} - /> - ) : ( - <> + {showActions && ( +
+
+ {message.streaming ? ( } - onClick={() => onResend(message)} - /> - - } - onClick={() => onDelete(message.id ?? i)} - /> - - } - onClick={() => onPinMessage(message)} - /> - } + text={Locale.Chat.Actions.Stop} + icon={} onClick={() => - copyToClipboard( - getMessageTextContent(message), - ) + onUserStop(message.id ?? i) } /> - {config.ttsConfig.enable && ( + ) : ( + <> - ) : ( - - ) + text={Locale.Chat.Actions.Retry} + icon={} + onClick={() => onResend(message)} + /> + + } + onClick={() => + onDelete(message.id ?? i) } + /> + + } + onClick={() => onPinMessage(message)} + /> + } onClick={() => - openaiSpeech( + copyToClipboard( getMessageTextContent(message), ) } /> - )} - - )} + {config.ttsConfig.enable && ( + + ) : ( + + ) + } + onClick={() => + openaiSpeech( + getMessageTextContent(message), + ) + } + /> + )} + + )} +
+ )} +
+ {message?.tools?.length == 0 && showTyping && ( +
+ {Locale.Chat.Typing}
)} -
- {message?.tools?.length == 0 && showTyping && ( -
- {Locale.Chat.Typing} -
- )} - {/*@ts-ignore*/} - {message?.tools?.length > 0 && ( -
- {message?.tools?.map((tool) => ( + {/*@ts-ignore*/} + {message?.tools?.length > 0 && ( +
+ {message?.tools?.map((tool) => ( +
+ {tool.isError === false ? ( + + ) : tool.isError === true ? ( + + ) : ( + + )} + {tool?.function?.name} +
+ ))} +
+ )} +
+ onRightClick(e, message)} // hard to use + onDoubleClickCapture={() => { + if (!isMobileScreen) return; + setUserInput(getMessageTextContent(message)); + }} + fontSize={fontSize} + fontFamily={fontFamily} + parentRef={scrollRef} + defaultShow={i >= messages.length - 6} + /> + {getMessageImages(message).length == 1 && ( + + )} + {getMessageImages(message).length > 1 && (
- {tool.isError === false ? ( - - ) : tool.isError === true ? ( - - ) : ( - + {getMessageImages(message).map( + (image, index) => { + return ( + + ); + }, )} - {tool?.function?.name}
- ))} + )}
- )} -
- onRightClick(e, message)} // hard to use - onDoubleClickCapture={() => { - if (!isMobileScreen) return; - setUserInput(getMessageTextContent(message)); - }} - fontSize={fontSize} - fontFamily={fontFamily} - parentRef={scrollRef} - defaultShow={i >= messages.length - 6} - /> - {getMessageImages(message).length == 1 && ( - - )} - {getMessageImages(message).length > 1 && ( -
- {getMessageImages(message).map((image, index) => { - return ( - - ); - })} + {message?.audio_url && ( +
+
)} -
- {message?.audio_url && ( -
-
- )} -
- {isContext - ? Locale.Chat.IsContext - : message.date.toLocaleString()} +
+ {isContext + ? Locale.Chat.IsContext + : message.date.toLocaleString()} +
-
- {shouldShowClearContextDivider && } - - ); - })} + {shouldShowClearContextDivider && } + + ); + })}
(); +const clientsMap = new Map< + string, + { client: Client; primitives: Primitive[] } +>(); // Whether initialized let initialized = false; @@ -30,8 +38,11 @@ export async function initializeMcpClients() { try { logger.info(`Initializing MCP client: ${clientId}`); const client = await createClient(config, clientId); - clientsMap.set(clientId, client); - logger.success(`Client ${clientId} initialized`); + const primitives = await listPrimitives(client); + clientsMap.set(clientId, { client, primitives }); + logger.success( + `Client [${clientId}] initialized, ${primitives.length} primitives supported`, + ); } catch (error) { errorClients.push(clientId); logger.error(`Failed to initialize client ${clientId}: ${error}`); @@ -58,7 +69,7 @@ export async function executeMcpAction( ) { try { // Find the corresponding client - const client = clientsMap.get(clientId); + const client = clientsMap.get(clientId)?.client; if (!client) { logger.error(`Client ${clientId} not found`); return; @@ -80,3 +91,16 @@ export async function getAvailableClients() { (clientId) => !errorClients.includes(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, + })); +} diff --git a/app/mcp/client.ts b/app/mcp/client.ts index 0600f00be92..6650f9e2b42 100644 --- a/app/mcp/client.ts +++ b/app/mcp/client.ts @@ -40,13 +40,13 @@ export async function createClient( return client; } -interface Primitive { +export interface Primitive { type: "resource" | "tool" | "prompt"; value: any; } /** List all resources, tools, and prompts */ -export async function listPrimitives(client: Client) { +export async function listPrimitives(client: Client): Promise { const capabilities = client.getServerCapabilities(); const primitives: Primitive[] = []; const promises = []; diff --git a/app/mcp/example.ts b/app/mcp/example.ts index 83fc8784cf6..f3b91fb8cbd 100644 --- a/app/mcp/example.ts +++ b/app/mcp/example.ts @@ -4,25 +4,25 @@ import conf from "./mcp_config.json"; const logger = new MCPClientLogger("MCP Server Example", true); +const TEST_SERVER = "everything"; + async function main() { - logger.info("Connecting to server..."); + logger.info(`All MCP servers: ${Object.keys(conf.mcpServers).join(", ")}`); + + logger.info(`Connecting to server ${TEST_SERVER}...`); - const client = await createClient(conf.mcpServers.everything, "everything"); + const client = await createClient(conf.mcpServers[TEST_SERVER], TEST_SERVER); const primitives = await listPrimitives(client); - logger.success(`Connected to server everything`); + logger.success(`Connected to server ${TEST_SERVER}`); logger.info( - `server capabilities: ${Object.keys( - client.getServerCapabilities() ?? [], - ).join(", ")}`, + `${TEST_SERVER} supported primitives:\n${JSON.stringify( + primitives.filter((i) => i.type === "tool"), + null, + 2, + )}`, ); - - logger.info("Server supports the following primitives:"); - - primitives.forEach((primitive) => { - logger.info("\n" + JSON.stringify(primitive, null, 2)); - }); } main().catch((error) => { diff --git a/app/store/chat.ts b/app/store/chat.ts index e0ee956219c..80c706ffd9d 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -21,6 +21,8 @@ import { DEFAULT_SYSTEM_TEMPLATE, GEMINI_SUMMARIZE_MODEL, KnowledgeCutOffDate, + MCP_PRIMITIVES_TEMPLATE, + MCP_SYSTEM_TEMPLATE, ServiceProvider, StoreKey, SUMMARIZE_MODEL, @@ -33,7 +35,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config"; import { useAccessStore } from "./access"; import { collectModelsWithDefaultModel } from "../utils/model"; import { createEmptyMask, Mask } from "./mask"; -import { executeMcpAction } from "../mcp/actions"; +import { executeMcpAction, getAllPrimitives } from "../mcp/actions"; import { extractMcpJson, isMcpJson } from "../mcp/utils"; const localStorage = safeLocalStorage(); @@ -196,6 +198,24 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) { return output; } +async function getMcpSystemPrompt(): Promise { + let primitives = await getAllPrimitives(); + primitives = primitives.filter((i) => + i.primitives.some((p) => p.type === "tool"), + ); + let primitivesString = ""; + primitives.forEach((i) => { + primitivesString += MCP_PRIMITIVES_TEMPLATE.replace( + "{{ clientId }}", + i.clientId, + ).replace( + "{{ primitives }}", + i.primitives.map((p) => JSON.stringify(p)).join("\n"), + ); + }); + return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_PRIMITIVES }}", primitivesString); +} + const DEFAULT_CHAT_STATE = { sessions: [createEmptySession()], currentSessionIndex: 0, @@ -409,7 +429,7 @@ export const useChatStore = createPersistStore( }); // get recent messages - const recentMessages = get().getMessagesWithMemory(); + const recentMessages = await get().getMessagesWithMemory(); const sendMessages = recentMessages.concat(userMessage); const messageIndex = session.messages.length + 1; @@ -508,7 +528,7 @@ export const useChatStore = createPersistStore( } }, - getMessagesWithMemory() { + async getMessagesWithMemory() { const session = get().currentSession(); const modelConfig = session.mask.modelConfig; const clearContextIndex = session.clearContextIndex ?? 0; @@ -524,18 +544,26 @@ export const useChatStore = createPersistStore( (session.mask.modelConfig.model.startsWith("gpt-") || session.mask.modelConfig.model.startsWith("chatgpt-")); + const mcpSystemPrompt = await getMcpSystemPrompt(); + var systemPrompts: ChatMessage[] = []; systemPrompts = shouldInjectSystemPrompts ? [ createMessage({ role: "system", - content: fillTemplateWith("", { - ...modelConfig, - template: DEFAULT_SYSTEM_TEMPLATE, - }), + content: + fillTemplateWith("", { + ...modelConfig, + template: DEFAULT_SYSTEM_TEMPLATE, + }) + mcpSystemPrompt, }), ] - : []; + : [ + createMessage({ + role: "system", + content: mcpSystemPrompt, + }), + ]; if (shouldInjectSystemPrompts) { console.log( "[Global System Prompt] ", @@ -796,12 +824,12 @@ export const useChatStore = createPersistStore( ? JSON.stringify(result) : String(result); get().onUserInput( - `\`\`\`json:mcp:${mcpRequest.clientId}\n${mcpResponse}\n\`\`\``, + `\`\`\`json:mcp-response:${mcpRequest.clientId}\n${mcpResponse}\n\`\`\``, [], true, ); }) - .catch((error) => showToast(String(error))); + .catch((error) => showToast("MCP execution failed", error)); } } catch (error) { console.error("[MCP Error]", error); From f2a2b40d2c07172db28cdd685fa8c9098c995acc Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 9 Jan 2025 10:20:56 +0800 Subject: [PATCH 07/29] feat: carry mcp primitives content as a system prompt --- app/constant.ts | 31 ++++++++++++++++++------------- app/store/chat.ts | 4 +++- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/app/constant.ts b/app/constant.ts index 544e2a24658..9d15b5fa11d 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -260,8 +260,6 @@ export const MCP_PRIMITIVES_TEMPLATE = ` {{ primitives }} `; -// String and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions. -// Here are the functions available in JSONSchema format: export const MCP_SYSTEM_TEMPLATE = ` You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed. @@ -269,7 +267,13 @@ You are an AI assistant with access to system tools. Your role is to help users {{ MCP_PRIMITIVES }} 2. WHEN TO USE TOOLS: - - When users ask any questions that can be answered by available tools, you should use the tools to answer the user's question. + - ALWAYS USE TOOLS when they can help answer user questions + - DO NOT just describe what you could do - TAKE ACTION immediately + - If you're not sure whether to use a tool, USE IT + - Common triggers for tool use: + * Questions about files or directories + * Requests to check, list, or manipulate system resources + * Any query that can be answered with available tools 3. HOW TO USE TOOLS: A. Tool Call Format: @@ -287,24 +291,25 @@ You are an AI assistant with access to system tools. Your role is to help users C. Important Rules: - Only ONE tool call per message - - Always use the exact primitive name from available tools + - ALWAYS TAKE ACTION instead of just describing what you could do - Include the correct clientId in code block language tag - Verify arguments match the primitive's requirements 4. INTERACTION FLOW: - A. Understand user's request - B. If tools are needed: - - Explain what you plan to do - - Make the appropriate tool call - - Wait for the response - - Explain the results in user-friendly terms + A. When user makes a request: + - IMMEDIATELY use appropriate tool if available + - DO NOT ask if user wants you to use the tool + - DO NOT just describe what you could do + B. After receiving tool response: + - Explain results clearly + - Take next appropriate action if needed C. If tools fail: - - Explain the error clearly - - Suggest alternatives or ask for clarification + - Explain the error + - Try alternative approach immediately 5. EXAMPLE INTERACTION: User: "What files do I have on my desktop?" - Assistant: "I'll first check which directories I have access to. + Assistant: "I'll check which directories I have access to. \`\`\`json:mcp:filesystem { "method": "tools/call", diff --git a/app/store/chat.ts b/app/store/chat.ts index 80c706ffd9d..93bbde99d64 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -203,6 +203,7 @@ async function getMcpSystemPrompt(): Promise { primitives = primitives.filter((i) => i.primitives.some((p) => p.type === "tool"), ); + let primitivesString = ""; primitives.forEach((i) => { primitivesString += MCP_PRIMITIVES_TEMPLATE.replace( @@ -210,9 +211,10 @@ async function getMcpSystemPrompt(): Promise { i.clientId, ).replace( "{{ primitives }}", - i.primitives.map((p) => JSON.stringify(p)).join("\n"), + i.primitives.map((p) => JSON.stringify(p, null, 2)).join("\n"), ); }); + return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_PRIMITIVES }}", primitivesString); } From 0c14ce6417821d512d04dec5a5755bf35deed51d Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 9 Jan 2025 13:41:17 +0800 Subject: [PATCH 08/29] fix: MCP execution content matching failed. --- app/mcp/mcp_config.json | 4 ++++ app/store/chat.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/mcp/mcp_config.json b/app/mcp/mcp_config.json index 6ad18236b52..3a8b3afaa83 100644 --- a/app/mcp/mcp_config.json +++ b/app/mcp/mcp_config.json @@ -11,6 +11,10 @@ "everything": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"] + }, + "docker-mcp": { + "command": "uvx", + "args": ["docker-mcp"] } } } diff --git a/app/store/chat.ts b/app/store/chat.ts index 93bbde99d64..4a70c9296c3 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -834,7 +834,7 @@ export const useChatStore = createPersistStore( .catch((error) => showToast("MCP execution failed", error)); } } catch (error) { - console.error("[MCP Error]", error); + console.error("[Check MCP JSON]", error); } } }, From 7d51bfd42e0f60a328abed353ab1ef717b6f3ba8 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 9 Jan 2025 19:51:01 +0800 Subject: [PATCH 09/29] feat: MCP market --- app/components/home.tsx | 12 +- app/components/mcp-market.module.scss | 612 ++++++++++++++++++++++++++ app/components/mcp-market.tsx | 564 ++++++++++++++++++++++++ app/components/sidebar.tsx | 10 + app/constant.ts | 1 + app/icons/mcp.svg | 15 + app/locales/cn.ts | 3 + app/mcp/actions.ts | 132 +++++- app/mcp/mcp_config.json | 24 +- app/mcp/preset-server.json | 206 +++++++++ app/mcp/types.ts | 38 ++ app/mcp/utils.ts | 6 +- next.config.mjs | 1 - yarn.lock | 13 +- 14 files changed, 1607 insertions(+), 30 deletions(-) create mode 100644 app/components/mcp-market.module.scss create mode 100644 app/components/mcp-market.tsx create mode 100644 app/icons/mcp.svg create mode 100644 app/mcp/preset-server.json diff --git a/app/components/home.tsx b/app/components/home.tsx index 5da49037885..32c5b4ac67a 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -2,7 +2,7 @@ require("../polyfill"); -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import styles from "./home.module.scss"; import BotIcon from "../icons/bot.svg"; @@ -18,8 +18,8 @@ import { getISOLang, getLang } from "../locales"; import { HashRouter as Router, - Routes, Route, + Routes, useLocation, } from "react-router-dom"; import { SideBar } from "./sidebar"; @@ -74,6 +74,13 @@ const Sd = dynamic(async () => (await import("./sd")).Sd, { loading: () => , }); +const McpMarketPage = dynamic( + async () => (await import("./mcp-market")).McpMarketPage, + { + loading: () => , + }, +); + export function useSwitchTheme() { const config = useAppConfig(); @@ -193,6 +200,7 @@ function Screen() { } /> } /> } /> + } /> diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss new file mode 100644 index 00000000000..5e4b6e9b015 --- /dev/null +++ b/app/components/mcp-market.module.scss @@ -0,0 +1,612 @@ +@import "../styles/animation.scss"; + +.mcp-market-page { + height: 100%; + display: flex; + flex-direction: column; + + .loading-indicator { + font-size: 12px; + color: var(--primary); + margin-left: 8px; + font-weight: normal; + opacity: 0.8; + } + + .mcp-market-page-body { + padding: 20px; + overflow-y: auto; + + .mcp-market-filter { + width: 100%; + max-width: 100%; + margin-bottom: 20px; + animation: slide-in ease 0.3s; + height: 40px; + display: flex; + + .search-bar { + flex-grow: 1; + max-width: 100%; + min-width: 0; + } + } + + .server-list { + display: flex; + flex-direction: column; + gap: 1px; + } + + .mcp-market-item { + display: flex; + justify-content: space-between; + padding: 20px; + border: var(--border-in-light); + animation: slide-in ease 0.3s; + background-color: var(--white); + transition: all 0.3s ease; + + &.disabled { + opacity: 0.7; + pointer-events: none; + } + + &:not(:last-child) { + border-bottom: 0; + } + + &:first-child { + border-top-left-radius: 10px; + border-top-right-radius: 10px; + } + + &:last-child { + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + } + + .mcp-market-header { + display: flex; + align-items: center; + + .mcp-market-title { + .mcp-market-name { + font-size: 14px; + font-weight: bold; + display: flex; + align-items: center; + gap: 8px; + + .server-status { + font-size: 12px; + padding: 2px 6px; + border-radius: 4px; + margin-left: 8px; + background-color: #10b981; + color: white; + + &.error { + background-color: #ef4444; + } + + &.waiting { + background-color: #f59e0b; + } + + .error-message { + font-size: 11px; + opacity: 0.9; + margin-left: 4px; + } + } + } + + .mcp-market-info { + font-size: 12px; + color: var(--black-50); + margin-top: 4px; + } + } + } + + .mcp-market-actions { + display: flex; + gap: 8px; + align-items: center; + + :global(.icon-button) { + transition: all 0.3s ease; + border: 1px solid transparent; + + &:hover { + transform: translateY(-1px); + filter: brightness(1.1); + } + + &.action-primary { + background-color: var(--primary); + color: white; + + svg { + filter: brightness(2); + } + + &:hover { + background-color: var(--primary); + border-color: var(--primary); + } + } + + &.action-warning { + background-color: var(--warning); + color: white; + + svg { + filter: brightness(2); + } + + &:hover { + background-color: var(--warning); + border-color: var(--warning); + } + } + + &.action-danger { + background-color: transparent; + color: var(--danger); + border-color: var(--danger); + + &:hover { + background-color: var(--danger); + color: white; + + svg { + filter: brightness(2); + } + } + } + + &.action-error { + color: #ef4444 !important; + border-color: #ef4444 !important; + } + } + } + + @media screen and (max-width: 600px) { + flex-direction: column; + gap: 10px; + + .mcp-market-actions { + justify-content: flex-end; + } + } + } + } + + .array-input { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + padding: 16px; + border: 1px solid var(--gray-200); + border-radius: 10px; + background-color: var(--white); + + .array-input-item { + display: flex; + gap: 8px; + align-items: center; + width: 100%; + padding: 0; + + input { + width: 100%; + padding: 8px 12px; + background-color: var(--gray-50); + border-radius: 6px; + transition: all 0.3s ease; + font-size: 13px; + border: 1px solid var(--gray-200); + + &:hover { + background-color: var(--gray-100); + border-color: var(--gray-300); + } + + &:focus { + background-color: var(--white); + border-color: var(--primary); + outline: none; + box-shadow: 0 0 0 2px var(--primary-10); + } + + &::placeholder { + color: var(--gray-300); + } + } + + :global(.icon-button) { + width: 32px; + height: 32px; + padding: 0; + border-radius: 6px; + background-color: transparent; + border: 1px solid var(--gray-200); + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background-color: var(--gray-100); + border-color: var(--gray-300); + } + + svg { + width: 16px; + height: 16px; + opacity: 0.7; + } + } + } + + :global(.icon-button.add-path-button) { + width: 100%; + background-color: var(--primary); + color: white; + padding: 8px 12px; + border-radius: 6px; + transition: all 0.3s ease; + margin-top: 8px; + display: flex; + align-items: center; + justify-content: center; + border: none; + height: 36px; + + &:hover { + background-color: var(--primary-dark); + } + + svg { + width: 16px; + height: 16px; + margin-right: 4px; + filter: brightness(2); + } + } + } + + .path-list { + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; + + .path-item { + display: flex; + gap: 10px; + width: 100%; + + input { + flex: 1; + width: 100%; + max-width: 100%; + padding: 10px; + border: var(--border-in-light); + border-radius: 10px; + box-sizing: border-box; + font-size: 14px; + background-color: var(--white); + color: var(--black); + + &:hover { + border-color: var(--gray-300); + } + + &:focus { + border-color: var(--primary); + outline: none; + box-shadow: 0 0 0 2px var(--primary-10); + } + + &::placeholder { + color: var(--gray-300) !important; + opacity: 1; + } + } + + .browse-button { + padding: 8px; + border: var(--border-in-light); + border-radius: 10px; + background-color: transparent; + color: var(--black-50); + + &:hover { + border-color: var(--primary); + color: var(--primary); + background-color: transparent; + } + + svg { + width: 16px; + height: 16px; + } + } + + .delete-button { + padding: 8px; + border: var(--border-in-light); + border-radius: 10px; + background-color: transparent; + color: var(--black-50); + + &:hover { + border-color: var(--danger); + color: var(--danger); + background-color: transparent; + } + + svg { + width: 16px; + height: 16px; + } + } + + .file-input { + display: none; + } + } + + .add-button { + align-self: flex-start; + display: flex; + align-items: center; + gap: 5px; + padding: 8px 12px; + background-color: transparent; + border: var(--border-in-light); + border-radius: 10px; + color: var(--black); + font-size: 12px; + margin-top: 5px; + + &:hover { + border-color: var(--primary); + color: var(--primary); + background-color: transparent; + } + + svg { + width: 16px; + height: 16px; + } + } + } + + .config-section { + width: 100%; + + .config-header { + margin-bottom: 12px; + + .config-title { + font-size: 14px; + font-weight: 600; + color: var(--black); + text-transform: capitalize; + } + + .config-description { + font-size: 12px; + color: var(--gray-500); + margin-top: 4px; + } + } + + .array-input { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + padding: 16px; + border: 1px solid var(--gray-200); + border-radius: 10px; + background-color: var(--white); + + .array-input-item { + display: flex; + gap: 8px; + align-items: center; + width: 100%; + padding: 0; + + input { + width: 100%; + padding: 8px 12px; + background-color: var(--gray-50); + border-radius: 6px; + transition: all 0.3s ease; + font-size: 13px; + border: 1px solid var(--gray-200); + + &:hover { + background-color: var(--gray-100); + border-color: var(--gray-300); + } + + &:focus { + background-color: var(--white); + border-color: var(--primary); + outline: none; + box-shadow: 0 0 0 2px var(--primary-10); + } + + &::placeholder { + color: var(--gray-300); + } + } + + :global(.icon-button) { + width: 32px; + height: 32px; + padding: 0; + border-radius: 6px; + background-color: transparent; + border: 1px solid var(--gray-200); + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background-color: var(--gray-100); + border-color: var(--gray-300); + } + + svg { + width: 16px; + height: 16px; + opacity: 0.7; + } + } + } + + :global(.icon-button.add-path-button) { + width: 100%; + background-color: var(--primary); + color: white; + padding: 8px 12px; + border-radius: 6px; + transition: all 0.3s ease; + margin-top: 8px; + display: flex; + align-items: center; + justify-content: center; + border: none; + height: 36px; + + &:hover { + background-color: var(--primary-dark); + } + + svg { + width: 16px; + height: 16px; + margin-right: 4px; + filter: brightness(2); + } + } + } + } + + .input-item { + width: 100%; + + input { + width: 100%; + padding: 10px; + border: var(--border-in-light); + border-radius: 10px; + box-sizing: border-box; + font-size: 14px; + background-color: var(--white); + color: var(--black); + + &:hover { + border-color: var(--gray-300); + } + + &:focus { + border-color: var(--primary); + outline: none; + box-shadow: 0 0 0 2px var(--primary-10); + } + + &::placeholder { + color: var(--gray-300) !important; + opacity: 1; + } + } + } + + .primitives-list { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + padding: 20px; + max-width: 100%; + overflow-x: hidden; + word-break: break-word; + box-sizing: border-box; + + .primitive-item { + width: 100%; + box-sizing: border-box; + + .primitive-name { + font-size: 14px; + font-weight: 600; + color: var(--black); + margin-bottom: 8px; + padding-left: 12px; + border-left: 3px solid var(--primary); + box-sizing: border-box; + width: 100%; + } + + .primitive-description { + font-size: 13px; + color: var(--gray-500); + line-height: 1.6; + padding-left: 15px; + box-sizing: border-box; + width: 100%; + } + } + } + + :global { + .modal-content { + margin-top: 20px; + max-width: 100%; + overflow-x: hidden; + } + + .list { + padding: 10px; + margin-bottom: 10px; + background-color: var(--white); + } + + .list-item { + border: none; + background-color: transparent; + border-radius: 10px; + padding: 10px; + margin-bottom: 10px; + + .list-header { + margin-bottom: 10px; + + .list-title { + font-size: 14px; + font-weight: bold; + text-transform: capitalize; + color: var(--black); + } + + .list-sub-title { + font-size: 12px; + color: var(--gray-500); + margin-top: 4px; + } + } + } + } +} diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx new file mode 100644 index 00000000000..5f0723e397c --- /dev/null +++ b/app/components/mcp-market.tsx @@ -0,0 +1,564 @@ +import { IconButton } from "./button"; +import { ErrorBoundary } from "./error"; +import styles from "./mcp-market.module.scss"; +import EditIcon from "../icons/edit.svg"; +import AddIcon from "../icons/add.svg"; +import CloseIcon from "../icons/close.svg"; +import DeleteIcon from "../icons/delete.svg"; +import RestartIcon from "../icons/reload.svg"; +import EyeIcon from "../icons/eye.svg"; +import { List, ListItem, Modal, showToast } from "./ui-lib"; +import { useNavigate } from "react-router-dom"; +import { useState, useEffect } from "react"; +import presetServersJson from "../mcp/preset-server.json"; +const presetServers = presetServersJson as PresetServer[]; +import { + getMcpConfig, + updateMcpConfig, + getClientPrimitives, + restartAllClients, + reinitializeMcpClients, + getClientErrors, +} from "../mcp/actions"; +import { McpConfig, PresetServer, ServerConfig } from "../mcp/types"; +import clsx from "clsx"; + +interface ConfigProperty { + type: string; + description?: string; + required?: boolean; + minItems?: number; +} + +export function McpMarketPage() { + const navigate = useNavigate(); + const [searchText, setSearchText] = useState(""); + const [config, setConfig] = useState({ mcpServers: {} }); + const [editingServerId, setEditingServerId] = useState(); + const [viewingServerId, setViewingServerId] = useState(); + const [primitives, setPrimitives] = useState([]); + const [userConfig, setUserConfig] = useState>({}); + const [isLoading, setIsLoading] = useState(false); + const [clientErrors, setClientErrors] = useState< + Record + >({}); + + // 更新服务器状态 + const updateServerStatus = async () => { + await reinitializeMcpClients(); + const errors = await getClientErrors(); + setClientErrors(errors); + }; + + // 初始加载配置 + useEffect(() => { + const init = async () => { + try { + setIsLoading(true); + const data = await getMcpConfig(); + setConfig(data); + await updateServerStatus(); + } catch (error) { + showToast("Failed to load configuration"); + console.error(error); + } finally { + setIsLoading(false); + } + }; + init(); + }, []); + + // 保存配置 + const saveConfig = async (newConfig: McpConfig) => { + try { + setIsLoading(true); + await updateMcpConfig(newConfig); + setConfig(newConfig); + await updateServerStatus(); + showToast("Configuration saved successfully"); + } catch (error) { + showToast("Failed to save configuration"); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + // 检查服务器是否已添加 + const isServerAdded = (id: string) => { + return id in config.mcpServers; + }; + + // 加载当前编辑服务器的配置 + useEffect(() => { + if (editingServerId) { + const currentConfig = config.mcpServers[editingServerId]; + if (currentConfig) { + // 从当前配置中提取用户配置 + const preset = presetServers.find((s) => s.id === editingServerId); + if (preset?.configSchema) { + const userConfig: Record = {}; + Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => { + if (mapping.type === "spread") { + // 对于 spread 类型,从 args 中提取数组 + const startPos = mapping.position ?? 0; + userConfig[key] = currentConfig.args.slice(startPos); + } else if (mapping.type === "single") { + // 对于 single 类型,获取单个值 + userConfig[key] = currentConfig.args[mapping.position ?? 0]; + } else if ( + mapping.type === "env" && + mapping.key && + currentConfig.env + ) { + // 对于 env 类型,从环境变量中获取值 + userConfig[key] = currentConfig.env[mapping.key]; + } + }); + setUserConfig(userConfig); + } + } else { + setUserConfig({}); + } + } + }, [editingServerId, config.mcpServers]); + + // 保存服务器配置 + const saveServerConfig = async () => { + const preset = presetServers.find((s) => s.id === editingServerId); + if (!preset || !preset.configSchema || !editingServerId) return; + + try { + // 构建服务器配置 + const args = [...preset.baseArgs]; + const env: Record = {}; + + Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => { + const value = userConfig[key]; + if (mapping.type === "spread" && Array.isArray(value)) { + const pos = mapping.position ?? 0; + args.splice(pos, 0, ...value); + } else if ( + mapping.type === "single" && + mapping.position !== undefined + ) { + args[mapping.position] = value; + } else if ( + mapping.type === "env" && + mapping.key && + typeof value === "string" + ) { + env[mapping.key] = value; + } + }); + + const serverConfig: ServerConfig = { + command: preset.command, + args, + ...(Object.keys(env).length > 0 ? { env } : {}), + }; + + // 更新配置 + const newConfig = { + ...config, + mcpServers: { + ...config.mcpServers, + [editingServerId]: serverConfig, + }, + }; + + await saveConfig(newConfig); + setEditingServerId(undefined); + showToast("Server configuration saved successfully"); + } catch (error) { + showToast( + error instanceof Error ? error.message : "Failed to save configuration", + ); + } + }; + + // 渲染配置表单 + const renderConfigForm = () => { + const preset = presetServers.find((s) => s.id === editingServerId); + if (!preset?.configSchema) return null; + + return Object.entries(preset.configSchema.properties).map( + ([key, prop]: [string, ConfigProperty]) => { + if (prop.type === "array") { + const currentValue = userConfig[key as keyof typeof userConfig] || []; + return ( + +
+ {(currentValue as string[]).map( + (value: string, index: number) => ( +
+ { + const newValue = [...currentValue] as string[]; + newValue[index] = e.target.value; + setUserConfig({ ...userConfig, [key]: newValue }); + }} + /> + } + className={styles["delete-button"]} + onClick={() => { + const newValue = [...currentValue] as string[]; + newValue.splice(index, 1); + setUserConfig({ ...userConfig, [key]: newValue }); + }} + /> +
+ ), + )} + } + text="Add Path" + className={styles["add-button"]} + bordered + onClick={() => { + const newValue = [...currentValue, ""] as string[]; + setUserConfig({ ...userConfig, [key]: newValue }); + }} + /> +
+
+ ); + } else if (prop.type === "string") { + const currentValue = userConfig[key as keyof typeof userConfig] || ""; + return ( + +
+ { + setUserConfig({ ...userConfig, [key]: e.target.value }); + }} + /> +
+
+ ); + } + return null; + }, + ); + }; + + // 获取服务器的 Primitives + const loadPrimitives = async (id: string) => { + try { + setIsLoading(true); + const result = await getClientPrimitives(id); + if (result) { + setPrimitives(result); + } else { + showToast("Server is not running"); + setPrimitives([]); + } + } catch (error) { + showToast("Failed to load primitives"); + console.error(error); + setPrimitives([]); + } finally { + setIsLoading(false); + } + }; + + // 重启所有客户端 + const handleRestart = async () => { + try { + setIsLoading(true); + await restartAllClients(); + await updateServerStatus(); + showToast("All clients restarted successfully"); + } catch (error) { + showToast("Failed to restart clients"); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + // 添加服务器 + const addServer = async (preset: PresetServer) => { + if (!preset.configurable) { + try { + setIsLoading(true); + showToast("Creating MCP client..."); + // 如果服务器不需要配置,直接添加 + const serverConfig: ServerConfig = { + command: preset.command, + args: [...preset.baseArgs], + }; + const newConfig = { + ...config, + mcpServers: { + ...config.mcpServers, + [preset.id]: serverConfig, + }, + }; + await saveConfig(newConfig); + } finally { + setIsLoading(false); + } + } else { + // 如果需要配置,打开配置对话框 + setEditingServerId(preset.id); + setUserConfig({}); + } + }; + + // 移除服务器 + const removeServer = async (id: string) => { + try { + setIsLoading(true); + const { [id]: _, ...rest } = config.mcpServers; + const newConfig = { + ...config, + mcpServers: rest, + }; + await saveConfig(newConfig); + } finally { + setIsLoading(false); + } + }; + + return ( + +
+
+
+
+ MCP Market + {isLoading && ( + Loading... + )} +
+
+ {Object.keys(config.mcpServers).length} servers configured +
+
+ +
+
+ } + bordered + onClick={handleRestart} + text="Restart" + disabled={isLoading} + /> +
+
+ } + bordered + onClick={() => navigate(-1)} + disabled={isLoading} + /> +
+
+
+ +
+
+ setSearchText(e.currentTarget.value)} + /> +
+ +
+ {presetServers + .filter( + (m) => + searchText.length === 0 || + m.name.toLowerCase().includes(searchText.toLowerCase()) || + m.description + .toLowerCase() + .includes(searchText.toLowerCase()), + ) + .sort((a, b) => { + const aAdded = isServerAdded(a.id); + const bAdded = isServerAdded(b.id); + const aError = clientErrors[a.id] !== null; + const bError = clientErrors[b.id] !== null; + + if (aAdded !== bAdded) { + return aAdded ? -1 : 1; + } + if (aAdded && bAdded) { + if (aError !== bError) { + return aError ? -1 : 1; + } + } + return 0; + }) + .map((server) => ( +
+
+
+
+ {server.name} + {isServerAdded(server.id) && ( + + {clientErrors[server.id] === null + ? "Active" + : "Error"} + {clientErrors[server.id] && ( + + : {clientErrors[server.id]} + + )} + + )} +
+
+ {server.description} +
+
+
+
+ {isServerAdded(server.id) ? ( + <> + {server.configurable && ( + } + text="Configure" + className={clsx({ + [styles["action-error"]]: + clientErrors[server.id] !== null, + })} + onClick={() => setEditingServerId(server.id)} + disabled={isLoading} + /> + )} + {isServerAdded(server.id) && ( + } + text="Detail" + onClick={async () => { + if (clientErrors[server.id] !== null) { + showToast("Server is not running"); + return; + } + setViewingServerId(server.id); + await loadPrimitives(server.id); + }} + disabled={isLoading} + /> + )} + } + text="Remove" + className={styles["action-danger"]} + onClick={() => removeServer(server.id)} + disabled={isLoading} + /> + + ) : ( + } + text="Add" + className={styles["action-primary"]} + onClick={() => addServer(server)} + disabled={isLoading} + /> + )} +
+
+ ))} +
+
+ + {editingServerId && ( +
+ !isLoading && setEditingServerId(undefined)} + actions={[ + setEditingServerId(undefined)} + bordered + disabled={isLoading} + />, + , + ]} + > + {renderConfigForm()} + +
+ )} + + {viewingServerId && ( +
+ setViewingServerId(undefined)} + actions={[ + setViewingServerId(undefined)} + bordered + />, + ]} + > +
+ {isLoading ? ( +
Loading...
+ ) : primitives.filter((p) => p.type === "tool").length > 0 ? ( + primitives + .filter((p) => p.type === "tool") + .map((primitive, index) => ( +
+
+ {primitive.value.name} +
+ {primitive.value.description && ( +
+ {primitive.value.description} +
+ )} +
+ )) + ) : ( +
No tools available
+ )} +
+
+
+ )} +
+
+ ); +} diff --git a/app/components/sidebar.tsx b/app/components/sidebar.tsx index a5e33b15ea3..84b0973bd93 100644 --- a/app/components/sidebar.tsx +++ b/app/components/sidebar.tsx @@ -9,6 +9,7 @@ import ChatGptIcon from "../icons/chatgpt.svg"; import AddIcon from "../icons/add.svg"; import DeleteIcon from "../icons/delete.svg"; import MaskIcon from "../icons/mask.svg"; +import McpIcon from "../icons/mcp.svg"; import DragIcon from "../icons/drag.svg"; import DiscoveryIcon from "../icons/discovery.svg"; @@ -250,6 +251,15 @@ export function SideBar(props: { className?: string }) { }} shadow /> + } + text={shouldNarrow ? undefined : Locale.Mcp.Name} + className={styles["sidebar-bar-button"]} + onClick={() => { + navigate(Path.McpMarket, { state: { fromHome: true } }); + }} + shadow + /> } text={shouldNarrow ? undefined : Locale.Discovery.Name} diff --git a/app/constant.ts b/app/constant.ts index 9d15b5fa11d..3c0ff6213aa 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -47,6 +47,7 @@ export enum Path { SdNew = "/sd-new", Artifacts = "/artifacts", SearchChat = "/search-chat", + McpMarket = "/mcp-market", } export enum ApiPath { diff --git a/app/icons/mcp.svg b/app/icons/mcp.svg new file mode 100644 index 00000000000..aaf0bbc7431 --- /dev/null +++ b/app/icons/mcp.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 47be019a809..bd8b530603e 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -626,6 +626,9 @@ const cn = { Discovery: { Name: "发现", }, + Mcp: { + Name: "MCP", + }, FineTuned: { Sysmessage: "你是一个助手", }, diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index ad07bb4288a..f9a6afc86e9 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -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 { + 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 { + 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 +> { + const status: Record = {}; + for (const [clientId, data] of clientsMap.entries()) { + status[clientId] = data.errorMsg; + } + return status; +} + +// 检查客户端状态 +export async function getClientErrors(): Promise< + Record +> { + const errors: Record = {}; + for (const [clientId, data] of clientsMap.entries()) { + errors[clientId] = data.errorMsg; + } + return errors; +} diff --git a/app/mcp/mcp_config.json b/app/mcp/mcp_config.json index 3a8b3afaa83..ee092d7f0f2 100644 --- a/app/mcp/mcp_config.json +++ b/app/mcp/mcp_config.json @@ -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"] } } } diff --git a/app/mcp/preset-server.json b/app/mcp/preset-server.json new file mode 100644 index 00000000000..0daec9aebf1 --- /dev/null +++ b/app/mcp/preset-server.json @@ -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 + } +] diff --git a/app/mcp/types.ts b/app/mcp/types.ts index 763121bad88..a97c94e059a 100644 --- a/app/mcp/types.ts +++ b/app/mcp/types.ts @@ -59,3 +59,41 @@ export const McpNotificationsSchema: z.ZodType = z.object({ method: z.string(), params: z.record(z.unknown()).optional(), }); + +// MCP 服务器配置相关类型 +export interface ServerConfig { + command: string; + args: string[]; + env?: Record; +} + +export interface McpConfig { + mcpServers: Record; +} + +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; +} diff --git a/app/mcp/utils.ts b/app/mcp/utils.ts index 5b6dcbf027f..b74509881ef 100644 --- a/app/mcp/utils.ts +++ b/app/mcp/utils.ts @@ -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; diff --git a/next.config.mjs b/next.config.mjs index 80241913929..0e1105d5647 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -32,7 +32,6 @@ const nextConfig = { }, experimental: { forceSwcTransforms: true, - serverActions: true, }, }; diff --git a/yarn.lock b/yarn.lock index 5b9741b2b4c..a99ff08041d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3076,15 +3076,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001579: - version "1.0.30001617" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz#809bc25f3f5027ceb33142a7d6c40759d7a901eb" - integrity sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA== - -caniuse-lite@^1.0.30001646: - version "1.0.30001649" - resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz#3ec700309ca0da2b0d3d5fb03c411b191761c992" - integrity sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ== +caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001646: + version "1.0.30001692" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz" + integrity sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A== ccount@^2.0.0: version "2.0.1" From b410ec399cefc78b7313ff387537edbe87ef4235 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 9 Jan 2025 20:02:27 +0800 Subject: [PATCH 10/29] feat: auto scroll to bottom when MCP response --- app/components/chat.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 75120041ce6..bbc4444f6bd 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -421,12 +421,11 @@ export function ChatAction(props: { function useScrollToBottom( scrollRef: RefObject, detach: boolean = false, + messages: ChatMessage[], ) { // for auto-scroll - const [autoScroll, setAutoScroll] = useState(true); - - function scrollDomToBottom() { + const scrollDomToBottom = useCallback(() => { const dom = scrollRef.current; if (dom) { requestAnimationFrame(() => { @@ -434,7 +433,7 @@ function useScrollToBottom( dom.scrollTo(0, dom.scrollHeight); }); } - } + }, [scrollRef]); // auto scroll useEffect(() => { @@ -443,6 +442,15 @@ function useScrollToBottom( } }); + // auto scroll when messages length changes + const lastMessagesLength = useRef(messages.length); + useEffect(() => { + if (messages.length > lastMessagesLength.current && !detach) { + scrollDomToBottom(); + } + lastMessagesLength.current = messages.length; + }, [messages.length, detach, scrollDomToBottom]); + return { scrollRef, autoScroll, @@ -978,6 +986,7 @@ function _Chat() { const { setAutoScroll, scrollDomToBottom } = useScrollToBottom( scrollRef, (isScrolledToBottom || isAttachWithTop) && !isTyping, + session.messages, ); const [hitBottom, setHitBottom] = useState(true); const isMobileScreen = useMobileScreen(); From 125a71feade05ad5f5a75dc8f979c1efc946cdab Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 9 Jan 2025 20:07:24 +0800 Subject: [PATCH 11/29] fix: unnecessary initialization --- app/components/mcp-market.tsx | 6 ++++-- app/mcp/actions.ts | 20 ++++++++++++++++++++ app/mcp/mcp_config.json | 4 ---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index 5f0723e397c..e754c413c67 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -17,8 +17,8 @@ import { updateMcpConfig, getClientPrimitives, restartAllClients, - reinitializeMcpClients, getClientErrors, + refreshClientStatus, } from "../mcp/actions"; import { McpConfig, PresetServer, ServerConfig } from "../mcp/types"; import clsx from "clsx"; @@ -45,7 +45,7 @@ export function McpMarketPage() { // 更新服务器状态 const updateServerStatus = async () => { - await reinitializeMcpClients(); + await refreshClientStatus(); const errors = await getClientErrors(); setClientErrors(errors); }; @@ -74,6 +74,8 @@ export function McpMarketPage() { setIsLoading(true); await updateMcpConfig(newConfig); setConfig(newConfig); + // 配置改变时需要重新初始化 + await restartAllClients(); await updateServerStatus(); showToast("Configuration saved successfully"); } catch (error) { diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index f9a6afc86e9..bf38dcc6311 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -214,3 +214,23 @@ export async function getClientErrors(): Promise< } 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); + } + } + + return { errorClients }; +} diff --git a/app/mcp/mcp_config.json b/app/mcp/mcp_config.json index ee092d7f0f2..e778108de5e 100644 --- a/app/mcp/mcp_config.json +++ b/app/mcp/mcp_config.json @@ -27,10 +27,6 @@ "playwright": { "command": "npx", "args": ["-y", "@executeautomation/playwright-mcp-server"] - }, - "gdrive": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-gdrive"] } } } From e95c94d7be72490668d8e022fd126cfe637b5f2a Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 9 Jan 2025 20:10:10 +0800 Subject: [PATCH 12/29] fix: inaccurate content --- app/components/mcp-market.tsx | 6 +++--- app/mcp/mcp_config.json | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index e754c413c67..926e64b297b 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -65,7 +65,7 @@ export function McpMarketPage() { setIsLoading(false); } }; - init(); + init().then(); }, []); // 保存配置 @@ -352,7 +352,7 @@ export function McpMarketPage() { icon={} bordered onClick={handleRestart} - text="Restart" + text="Restart All" disabled={isLoading} />
@@ -458,7 +458,7 @@ export function McpMarketPage() { {isServerAdded(server.id) && ( } - text="Detail" + text="Tools" onClick={async () => { if (clientErrors[server.id] !== null) { showToast("Server is not running"); diff --git a/app/mcp/mcp_config.json b/app/mcp/mcp_config.json index e778108de5e..ee092d7f0f2 100644 --- a/app/mcp/mcp_config.json +++ b/app/mcp/mcp_config.json @@ -27,6 +27,10 @@ "playwright": { "command": "npx", "args": ["-y", "@executeautomation/playwright-mcp-server"] + }, + "gdrive": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-gdrive"] } } } From a3af563e894286654bf1e7cf1f66190d9c467a79 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 9 Jan 2025 20:13:16 +0800 Subject: [PATCH 13/29] feat: Reset mcp_config.json to empty --- app/mcp/mcp_config.json | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/app/mcp/mcp_config.json b/app/mcp/mcp_config.json index ee092d7f0f2..da39e4ffafe 100644 --- a/app/mcp/mcp_config.json +++ b/app/mcp/mcp_config.json @@ -1,36 +1,3 @@ { - "mcpServers": { - "filesystem": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-filesystem", - "/Users/kadxy/Desktop" - ] - }, - "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"] - } - } + "mcpServers": {} } From ce13cf61a74f7b0682c230efed2742db91c7d1b7 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 9 Jan 2025 20:15:47 +0800 Subject: [PATCH 14/29] feat: ignore mcp_config.json --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2ff556f646e..b1c2bfefad3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ dev *.key.pub masks.json + +# mcp config +app/mcp/mcp_config.json From 8aa9a500fdee762abe5fd8e0bba00065be1725f4 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Wed, 15 Jan 2025 16:52:54 +0800 Subject: [PATCH 15/29] feat: Optimize MCP configuration logic --- app/components/chat.tsx | 24 ++ app/components/home.tsx | 10 + app/components/mcp-market.module.scss | 188 ++++----- app/components/mcp-market.tsx | 587 ++++++++++++++------------ app/constant.ts | 11 +- app/icons/tool.svg | 5 + app/mcp/actions.ts | 335 +++++++-------- app/mcp/client.ts | 70 +-- app/mcp/example.ts | 14 +- app/mcp/mcp_config.json | 13 +- app/mcp/preset-server.json | 26 +- app/mcp/types.ts | 60 ++- app/page.tsx | 5 +- app/store/chat.ts | 25 +- 14 files changed, 743 insertions(+), 630 deletions(-) create mode 100644 app/icons/tool.svg diff --git a/app/components/chat.tsx b/app/components/chat.tsx index bbc4444f6bd..c8d6886e562 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -46,6 +46,7 @@ import QualityIcon from "../icons/hd.svg"; import StyleIcon from "../icons/palette.svg"; import PluginIcon from "../icons/plugin.svg"; import ShortcutkeyIcon from "../icons/shortcutkey.svg"; +import McpToolIcon from "../icons/tool.svg"; import HeadphoneIcon from "../icons/headphone.svg"; import { BOT_HELLO, @@ -121,6 +122,7 @@ import { isEmpty } from "lodash-es"; import { getModelProvider } from "../utils/model"; import { RealtimeChat } from "@/app/components/realtime-chat"; import clsx from "clsx"; +import { getAvailableClientsCount } from "../mcp/actions"; const localStorage = safeLocalStorage(); @@ -130,6 +132,27 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { loading: () => , }); +const MCPAction = () => { + const navigate = useNavigate(); + const [count, setCount] = useState(0); + + useEffect(() => { + const loadCount = async () => { + const count = await getAvailableClientsCount(); + setCount(count); + }; + loadCount(); + }, []); + + return ( + navigate(Path.McpMarket)} + text={`MCP${count ? ` (${count})` : ""}`} + icon={} + /> + ); +}; + export function SessionConfigModel(props: { onClose: () => void }) { const chatStore = useChatStore(); const session = chatStore.currentSession(); @@ -799,6 +822,7 @@ export function ChatActions(props: { icon={} /> )} + {!isMobileScreen && }
{config.realtimeConfig.enable && ( diff --git a/app/components/home.tsx b/app/components/home.tsx index 32c5b4ac67a..8a03c50b6dc 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -29,6 +29,8 @@ import { getClientConfig } from "../config/client"; import { type ClientApi, getClientApi } from "../client/api"; import { useAccessStore } from "../store"; import clsx from "clsx"; +import { initializeMcpSystem } from "../mcp/actions"; +import { showToast } from "./ui-lib"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -243,6 +245,14 @@ export function Home() { useAccessStore.getState().fetch(); }, []); + useEffect(() => { + // 初始化 MCP 系统 + initializeMcpSystem().catch((error) => { + console.error("Failed to initialize MCP system:", error); + showToast("Failed to initialize MCP system"); + }); + }, []); + if (!useHasHydrated()) { return ; } diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss index 5e4b6e9b015..93c6b67de6a 100644 --- a/app/components/mcp-market.module.scss +++ b/app/components/mcp-market.module.scss @@ -39,8 +39,6 @@ } .mcp-market-item { - display: flex; - justify-content: space-between; padding: 20px; border: var(--border-in-light); animation: slide-in ease 0.3s; @@ -68,118 +66,106 @@ .mcp-market-header { display: flex; - align-items: center; + justify-content: space-between; + align-items: flex-start; + width: 100%; .mcp-market-title { - .mcp-market-name { - font-size: 14px; - font-weight: bold; - display: flex; + flex-grow: 1; + margin-right: 20px; + max-width: calc(100% - 300px); + } + + .mcp-market-name { + font-size: 14px; + font-weight: bold; + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + + .server-status { + display: inline-flex; align-items: center; - gap: 8px; + margin-left: 10px; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + background-color: #22c55e; + color: #fff; - .server-status { - font-size: 12px; - padding: 2px 6px; - border-radius: 4px; - margin-left: 8px; - background-color: #10b981; - color: white; - - &.error { - background-color: #ef4444; - } - - &.waiting { - background-color: #f59e0b; - } - - .error-message { - font-size: 11px; - opacity: 0.9; - margin-left: 4px; - } + &.error { + background-color: #ef4444; } - } - .mcp-market-info { - font-size: 12px; - color: var(--black-50); - margin-top: 4px; + .error-message { + margin-left: 4px; + font-size: 12px; + } } } - } - - .mcp-market-actions { - display: flex; - gap: 8px; - align-items: center; - :global(.icon-button) { - transition: all 0.3s ease; - border: 1px solid transparent; + .repo-link { + color: var(--primary); + font-size: 12px; + display: inline-flex; + align-items: center; + gap: 4px; + text-decoration: none; + opacity: 0.8; + transition: opacity 0.2s; &:hover { - transform: translateY(-1px); - filter: brightness(1.1); + opacity: 1; } - &.action-primary { - background-color: var(--primary); - color: white; - - svg { - filter: brightness(2); - } - - &:hover { - background-color: var(--primary); - border-color: var(--primary); - } + svg { + width: 14px; + height: 14px; } + } - &.action-warning { - background-color: var(--warning); - color: white; + .tags-container { + display: flex; + gap: 4px; + flex-wrap: wrap; + margin-bottom: 8px; + } - svg { - filter: brightness(2); - } + .tag { + background: var(--gray); + color: var(--black); + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + opacity: 0.8; + } - &:hover { - background-color: var(--warning); - border-color: var(--warning); - } - } + .mcp-market-info { + color: var(--black); + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .mcp-market-actions { + display: flex; + gap: 8px; + align-items: flex-start; + flex-shrink: 0; + min-width: 180px; + justify-content: flex-end; - &.action-danger { - background-color: transparent; - color: var(--danger); - border-color: var(--danger); + :global(.icon-button) { + transition: all 0.3s ease; + border: 1px solid transparent; &:hover { - background-color: var(--danger); - color: white; - - svg { - filter: brightness(2); - } + transform: translateY(-1px); + filter: brightness(1.1); } } - - &.action-error { - color: #ef4444 !important; - border-color: #ef4444 !important; - } - } - } - - @media screen and (max-width: 600px) { - flex-direction: column; - gap: 10px; - - .mcp-market-actions { - justify-content: flex-end; } } } @@ -312,11 +298,6 @@ outline: none; box-shadow: 0 0 0 2px var(--primary-10); } - - &::placeholder { - color: var(--gray-300) !important; - opacity: 1; - } } .browse-button { @@ -534,7 +515,7 @@ } } - .primitives-list { + .tools-list { display: flex; flex-direction: column; gap: 16px; @@ -545,11 +526,11 @@ word-break: break-word; box-sizing: border-box; - .primitive-item { + .tool-item { width: 100%; box-sizing: border-box; - .primitive-name { + .tool-name { font-size: 14px; font-weight: 600; color: var(--black); @@ -560,7 +541,7 @@ width: 100%; } - .primitive-description { + .tool-description { font-size: 13px; color: var(--gray-500); line-height: 1.6; @@ -590,9 +571,12 @@ border-radius: 10px; padding: 10px; margin-bottom: 10px; + display: flex; + flex-direction: column; + gap: 10px; .list-header { - margin-bottom: 10px; + margin-bottom: 0; .list-title { font-size: 14px; diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index 926e64b297b..d93754549e3 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -7,22 +7,29 @@ import CloseIcon from "../icons/close.svg"; import DeleteIcon from "../icons/delete.svg"; import RestartIcon from "../icons/reload.svg"; import EyeIcon from "../icons/eye.svg"; +import GithubIcon from "../icons/github.svg"; import { List, ListItem, Modal, showToast } from "./ui-lib"; import { useNavigate } from "react-router-dom"; -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; import presetServersJson from "../mcp/preset-server.json"; -const presetServers = presetServersJson as PresetServer[]; import { - getMcpConfig, - updateMcpConfig, - getClientPrimitives, + addMcpServer, + getClientStatus, + getClientTools, + getMcpConfigFromFile, + removeMcpServer, restartAllClients, - getClientErrors, - refreshClientStatus, } from "../mcp/actions"; -import { McpConfig, PresetServer, ServerConfig } from "../mcp/types"; +import { + ListToolsResponse, + McpConfigData, + PresetServer, + ServerConfig, +} from "../mcp/types"; import clsx from "clsx"; +const presetServers = presetServersJson as PresetServer[]; + interface ConfigProperty { type: string; description?: string; @@ -33,67 +40,71 @@ interface ConfigProperty { export function McpMarketPage() { const navigate = useNavigate(); const [searchText, setSearchText] = useState(""); - const [config, setConfig] = useState({ mcpServers: {} }); + const [userConfig, setUserConfig] = useState>({}); const [editingServerId, setEditingServerId] = useState(); + const [tools, setTools] = useState(null); const [viewingServerId, setViewingServerId] = useState(); - const [primitives, setPrimitives] = useState([]); - const [userConfig, setUserConfig] = useState>({}); const [isLoading, setIsLoading] = useState(false); - const [clientErrors, setClientErrors] = useState< - Record + const [config, setConfig] = useState(); + const [clientStatuses, setClientStatuses] = useState< + Record< + string, + { + status: "active" | "error" | "undefined"; + errorMsg: string | null; + } + > >({}); - // 更新服务器状态 - const updateServerStatus = async () => { - await refreshClientStatus(); - const errors = await getClientErrors(); - setClientErrors(errors); + // 检查服务器是否已添加 + const isServerAdded = (id: string) => { + return id in (config?.mcpServers ?? {}); + }; + + // 获取客户端状态 + const updateClientStatus = async (clientId: string) => { + const status = await getClientStatus(clientId); + setClientStatuses((prev) => ({ + ...prev, + [clientId]: status, + })); + return status; }; - // 初始加载配置 + // 从服务器获取初始状态 useEffect(() => { - const init = async () => { + const loadInitialState = async () => { try { setIsLoading(true); - const data = await getMcpConfig(); - setConfig(data); - await updateServerStatus(); + const config = await getMcpConfigFromFile(); + setConfig(config); + + // 获取所有客户端的状态 + const statuses: Record = {}; + for (const clientId of Object.keys(config.mcpServers)) { + const status = await getClientStatus(clientId); + statuses[clientId] = status; + } + setClientStatuses(statuses); } catch (error) { - showToast("Failed to load configuration"); - console.error(error); + console.error("Failed to load initial state:", error); + showToast("Failed to load initial state"); } finally { setIsLoading(false); } }; - init().then(); + loadInitialState(); }, []); - // 保存配置 - const saveConfig = async (newConfig: McpConfig) => { - try { - setIsLoading(true); - await updateMcpConfig(newConfig); - setConfig(newConfig); - // 配置改变时需要重新初始化 - await restartAllClients(); - await updateServerStatus(); - showToast("Configuration saved successfully"); - } catch (error) { - showToast("Failed to save configuration"); - console.error(error); - } finally { - setIsLoading(false); - } - }; - - // 检查服务器是否已添加 - const isServerAdded = (id: string) => { - return id in config.mcpServers; - }; + // Debug: 监控状态变化 + useEffect(() => { + console.log("MCP Market - Current config:", config); + console.log("MCP Market - Current clientStatuses:", clientStatuses); + }, [config, clientStatuses]); // 加载当前编辑服务器的配置 useEffect(() => { - if (editingServerId) { + if (editingServerId && config) { const currentConfig = config.mcpServers[editingServerId]; if (currentConfig) { // 从当前配置中提取用户配置 @@ -123,7 +134,7 @@ export function McpMarketPage() { setUserConfig({}); } } - }, [editingServerId, config.mcpServers]); + }, [editingServerId, config]); // 保存服务器配置 const saveServerConfig = async () => { @@ -131,6 +142,7 @@ export function McpMarketPage() { if (!preset || !preset.configSchema || !editingServerId) return; try { + setIsLoading(true); // 构建服务器配置 const args = [...preset.baseArgs]; const env: Record = {}; @@ -160,22 +172,113 @@ export function McpMarketPage() { ...(Object.keys(env).length > 0 ? { env } : {}), }; - // 更新配置 - const newConfig = { - ...config, - mcpServers: { - ...config.mcpServers, - [editingServerId]: serverConfig, - }, - }; + // 更新配置并初始化新服务器 + const newConfig = await addMcpServer(editingServerId, serverConfig); + setConfig(newConfig); + + // 更新状态 + const status = await getClientStatus(editingServerId); + setClientStatuses((prev) => ({ + ...prev, + [editingServerId]: status, + })); - await saveConfig(newConfig); setEditingServerId(undefined); showToast("Server configuration saved successfully"); } catch (error) { showToast( error instanceof Error ? error.message : "Failed to save configuration", ); + } finally { + setIsLoading(false); + } + }; + + // 获取服务器支持的 Tools + const loadTools = async (id: string) => { + try { + const result = await getClientTools(id); + if (result) { + setTools(result); + } else { + throw new Error("Failed to load tools"); + } + } catch (error) { + showToast("Failed to load tools"); + console.error(error); + setTools(null); + } + }; + + // 重启所有客户端 + const handleRestartAll = async () => { + try { + setIsLoading(true); + const newConfig = await restartAllClients(); + setConfig(newConfig); + + // 更新所有客户端状态 + const statuses: Record = {}; + for (const clientId of Object.keys(newConfig.mcpServers)) { + const status = await getClientStatus(clientId); + statuses[clientId] = status; + } + setClientStatuses(statuses); + + showToast("Successfully restarted all clients"); + } catch (error) { + showToast("Failed to restart clients"); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + // 添加服务器 + const addServer = async (preset: PresetServer) => { + if (!preset.configurable) { + try { + setIsLoading(true); + showToast("Creating MCP client..."); + // 如果服务器不需要配置,直接添加 + const serverConfig: ServerConfig = { + command: preset.command, + args: [...preset.baseArgs], + }; + const newConfig = await addMcpServer(preset.id, serverConfig); + setConfig(newConfig); + + // 更新状态 + const status = await getClientStatus(preset.id); + setClientStatuses((prev) => ({ + ...prev, + [preset.id]: status, + })); + } finally { + setIsLoading(false); + } + } else { + // 如果需要配置,打开配置对话框 + setEditingServerId(preset.id); + setUserConfig({}); + } + }; + + // 移除服务器 + const removeServer = async (id: string) => { + try { + setIsLoading(true); + const newConfig = await removeMcpServer(id); + setConfig(newConfig); + + // 移除状态 + setClientStatuses((prev) => { + const newStatuses = { ...prev }; + delete newStatuses[id]; + return newStatuses; + }); + } finally { + setIsLoading(false); } }; @@ -188,8 +291,17 @@ export function McpMarketPage() { ([key, prop]: [string, ConfigProperty]) => { if (prop.type === "array") { const currentValue = userConfig[key as keyof typeof userConfig] || []; + const itemLabel = (prop as any).itemLabel || key; + const addButtonText = + (prop as any).addButtonText || `Add ${itemLabel}`; + return ( - +
{(currentValue as string[]).map( (value: string, index: number) => ( @@ -197,7 +309,7 @@ export function McpMarketPage() { { const newValue = [...currentValue] as string[]; newValue[index] = e.target.value; @@ -218,7 +330,7 @@ export function McpMarketPage() { )} } - text="Add Path" + text={addButtonText} className={styles["add-button"]} bordered onClick={() => { @@ -251,83 +363,146 @@ export function McpMarketPage() { ); }; - // 获取服务器的 Primitives - const loadPrimitives = async (id: string) => { - try { - setIsLoading(true); - const result = await getClientPrimitives(id); - if (result) { - setPrimitives(result); - } else { - showToast("Server is not running"); - setPrimitives([]); - } - } catch (error) { - showToast("Failed to load primitives"); - console.error(error); - setPrimitives([]); - } finally { - setIsLoading(false); - } + // 检查服务器状态 + const checkServerStatus = (clientId: string) => { + return clientStatuses[clientId] || { status: "undefined", errorMsg: null }; }; - // 重启所有客户端 - const handleRestart = async () => { - try { - setIsLoading(true); - await restartAllClients(); - await updateServerStatus(); - showToast("All clients restarted successfully"); - } catch (error) { - showToast("Failed to restart clients"); - console.error(error); - } finally { - setIsLoading(false); - } - }; + // 渲染服务器列表 + const renderServerList = () => { + return presetServers + .filter((server) => { + if (searchText.length === 0) return true; + const searchLower = searchText.toLowerCase(); + return ( + server.name.toLowerCase().includes(searchLower) || + server.description.toLowerCase().includes(searchLower) || + server.tags.some((tag) => tag.toLowerCase().includes(searchLower)) + ); + }) + .sort((a, b) => { + const aStatus = checkServerStatus(a.id).status; + const bStatus = checkServerStatus(b.id).status; - // 添加服务器 - const addServer = async (preset: PresetServer) => { - if (!preset.configurable) { - try { - setIsLoading(true); - showToast("Creating MCP client..."); - // 如果服务器不需要配置,直接添加 - const serverConfig: ServerConfig = { - command: preset.command, - args: [...preset.baseArgs], + // 定义状态优先级 + const statusPriority = { + error: 0, + active: 1, + undefined: 2, }; - const newConfig = { - ...config, - mcpServers: { - ...config.mcpServers, - [preset.id]: serverConfig, - }, - }; - await saveConfig(newConfig); - } finally { - setIsLoading(false); - } - } else { - // 如果需要配置,打开配置对话框 - setEditingServerId(preset.id); - setUserConfig({}); - } - }; - // 移除服务器 - const removeServer = async (id: string) => { - try { - setIsLoading(true); - const { [id]: _, ...rest } = config.mcpServers; - const newConfig = { - ...config, - mcpServers: rest, - }; - await saveConfig(newConfig); - } finally { - setIsLoading(false); - } + // 首先按状态排序 + if (aStatus !== bStatus) { + return statusPriority[aStatus] - statusPriority[bStatus]; + } + + // 然后按名称排序 + return a.name.localeCompare(b.name); + }) + .map((server) => ( +
+
+
+
+ {server.name} + {checkServerStatus(server.id).status !== "undefined" && ( + + {checkServerStatus(server.id).status === "error" ? ( + <> + Error + + : {checkServerStatus(server.id).errorMsg} + + + ) : ( + "Active" + )} + + )} + {server.repo && ( + + + + )} +
+
+ {server.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+ {server.description} +
+
+
+ {isServerAdded(server.id) ? ( + <> + {server.configurable && ( + } + text="Configure" + className={clsx({ + [styles["action-error"]]: + checkServerStatus(server.id).status === "error", + })} + onClick={() => setEditingServerId(server.id)} + disabled={isLoading} + /> + )} + } + text="Tools" + onClick={async () => { + setViewingServerId(server.id); + await loadTools(server.id); + }} + disabled={ + isLoading || + checkServerStatus(server.id).status === "error" + } + /> + } + text="Remove" + className={styles["action-danger"]} + onClick={() => removeServer(server.id)} + disabled={isLoading} + /> + + ) : ( + } + text="Add" + className={styles["action-primary"]} + onClick={() => addServer(server)} + disabled={isLoading} + /> + )} +
+
+
+ )); }; return ( @@ -342,7 +517,7 @@ export function McpMarketPage() { )}
- {Object.keys(config.mcpServers).length} servers configured + {Object.keys(config?.mcpServers ?? {}).length} servers configured
@@ -351,7 +526,7 @@ export function McpMarketPage() { } bordered - onClick={handleRestart} + onClick={handleRestartAll} text="Restart All" disabled={isLoading} /> @@ -378,121 +553,10 @@ export function McpMarketPage() { />
-
- {presetServers - .filter( - (m) => - searchText.length === 0 || - m.name.toLowerCase().includes(searchText.toLowerCase()) || - m.description - .toLowerCase() - .includes(searchText.toLowerCase()), - ) - .sort((a, b) => { - const aAdded = isServerAdded(a.id); - const bAdded = isServerAdded(b.id); - const aError = clientErrors[a.id] !== null; - const bError = clientErrors[b.id] !== null; - - if (aAdded !== bAdded) { - return aAdded ? -1 : 1; - } - if (aAdded && bAdded) { - if (aError !== bError) { - return aError ? -1 : 1; - } - } - return 0; - }) - .map((server) => ( -
-
-
-
- {server.name} - {isServerAdded(server.id) && ( - - {clientErrors[server.id] === null - ? "Active" - : "Error"} - {clientErrors[server.id] && ( - - : {clientErrors[server.id]} - - )} - - )} -
-
- {server.description} -
-
-
-
- {isServerAdded(server.id) ? ( - <> - {server.configurable && ( - } - text="Configure" - className={clsx({ - [styles["action-error"]]: - clientErrors[server.id] !== null, - })} - onClick={() => setEditingServerId(server.id)} - disabled={isLoading} - /> - )} - {isServerAdded(server.id) && ( - } - text="Tools" - onClick={async () => { - if (clientErrors[server.id] !== null) { - showToast("Server is not running"); - return; - } - setViewingServerId(server.id); - await loadPrimitives(server.id); - }} - disabled={isLoading} - /> - )} - } - text="Remove" - className={styles["action-danger"]} - onClick={() => removeServer(server.id)} - disabled={isLoading} - /> - - ) : ( - } - text="Add" - className={styles["action-primary"]} - onClick={() => addServer(server)} - disabled={isLoading} - /> - )} -
-
- ))} -
+
{renderServerList()}
+ {/*编辑服务器配置*/} {editingServerId && (
)} + {/*支持的Tools*/} {viewingServerId && (
, ]} > -
+
{isLoading ? (
Loading...
- ) : primitives.filter((p) => p.type === "tool").length > 0 ? ( - primitives - .filter((p) => p.type === "tool") - .map((primitive, index) => ( -
-
- {primitive.value.name} + ) : tools?.tools ? ( + tools.tools.map( + (tool: ListToolsResponse["tools"], index: number) => ( +
+
{tool.name}
+
+ {tool.description}
- {primitive.value.description && ( -
- {primitive.value.description} -
- )}
- )) + ), + ) ) : (
No tools available
)} diff --git a/app/constant.ts b/app/constant.ts index 3c0ff6213aa..9cdf197bfba 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -88,6 +88,7 @@ export enum StoreKey { Update = "chat-update", Sync = "sync", SdList = "sd-list", + Mcp = "mcp-store", } export const DEFAULT_SIDEBAR_WIDTH = 300; @@ -254,18 +255,18 @@ Latex inline: \\(x^2\\) Latex block: $$e=mc^2$$ `; -export const MCP_PRIMITIVES_TEMPLATE = ` +export const MCP_TOOLS_TEMPLATE = ` [clientId] {{ clientId }} -[primitives] -{{ primitives }} +[tools] +{{ tools }} `; export const MCP_SYSTEM_TEMPLATE = ` You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed. -1. TOOLS AVAILABLE: -{{ MCP_PRIMITIVES }} +1. AVAILABLE TOOLS: +{{ MCP_TOOLS }} 2. WHEN TO USE TOOLS: - ALWAYS USE TOOLS when they can help answer user questions diff --git a/app/icons/tool.svg b/app/icons/tool.svg new file mode 100644 index 00000000000..f7543e2013d --- /dev/null +++ b/app/icons/tool.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index bf38dcc6311..6b5ea6df358 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -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"); +const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json"); -// Use Map to store all clients -const clientsMap = new Map< - string, - { client: Client | null; primitives: Primitive[]; errorMsg: string | null } ->(); +const clientsMap = new Map(); -// Whether initialized -let initialized = false; +// 获取客户端状态 +export async function getClientStatus(clientId: string) { + const status = clientsMap.get(clientId); + if (!status) return { status: "undefined" as const, errorMsg: null }; -// Store failed clients -let errorClients: string[] = []; + return { + status: status.errorMsg ? ("error" as const) : ("active" as const), + errorMsg: status.errorMsg, + }; +} -const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json"); +// 获取客户端工具 +export async function getClientTools(clientId: string) { + return clientsMap.get(clientId)?.tools ?? null; +} -// 获取 MCP 配置 -export async function getMcpConfig(): Promise { - 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: {} }; +// 获取可用客户端数量 +export async function getAvailableClientsCount() { + let count = 0; + clientsMap.forEach((map) => { + if (!map.errorMsg) { + count += map?.tools?.tools?.length ?? 0; + } + }); + return count; +} + +// 获取所有客户端工具 +export async function getAllTools() { + const result = []; + for (const [clientId, status] of clientsMap.entries()) { + result.push({ + clientId, + tools: status.tools, + }); } + return result; } -// 更新 MCP 配置 -export async function updateMcpConfig(config: McpConfig): Promise { +// 初始化单个客户端 +async function initializeSingleClient( + clientId: string, + serverConfig: ServerConfig, +) { + logger.info(`Initializing client [${clientId}]...`); try { - await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2)); + 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) { - console.error("Failed to write MCP config:", error); - throw 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 reinitializeMcpClients() { - logger.info("Reinitializing MCP clients..."); - // 遍历所有客户端,关闭 +// 初始化系统 +export async function initializeMcpSystem() { + logger.info("MCP Actions starting..."); try { - for (const [clientId, clientData] of clientsMap.entries()) { - clientData.client?.close(); + 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 close clients: ${error}`); + logger.error(`Failed to initialize MCP system: ${error}`); + throw error; } - // 清空状态 - clientsMap.clear(); - errorClients = []; - initialized = false; - // 重新初始化 - return initializeMcpClients(); } -// Initialize all configured clients -export async function initializeMcpClients() { - // If already initialized, return - if (initialized) { - return { errorClients }; +// 添加服务器 +export async function addMcpServer(clientId: string, config: ServerConfig) { + try { + const currentConfig = await getMcpConfigFromFile(); + const newConfig = { + ...currentConfig, + mcpServers: { + ...currentConfig.mcpServers, + [clientId]: config, + }, + }; + await updateMcpConfig(newConfig); + // 只初始化新添加的服务器 + await initializeSingleClient(clientId, config); + return newConfig; + } catch (error) { + logger.error(`Failed to add server [${clientId}]: ${error}`); + throw error; } +} - 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}`); +// 移除服务器 +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); } - } - - initialized = true; + clientsMap.delete(clientId); - if (errorClients.length > 0) { - logger.warn(`Failed to initialize clients: ${errorClients.join(", ")}`); - } else { - logger.success("All MCP clients initialized"); + return newConfig; + } catch (error) { + logger.error(`Failed to remove server [${clientId}]: ${error}`); + throw error; } +} - const availableClients = await getAvailableClients(); - logger.info(`Available clients: ${availableClients.join(",")}`); +// 重启所有客户端 +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(); - 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; + } } -// Execute MCP request +// 执行 MCP 请求 export async function executeMcpAction( clientId: string, request: McpRequestMessage, ) { try { - // Find the corresponding client - const client = clientsMap.get(clientId)?.client; - if (!client) { - logger.error(`Client ${clientId} not found`); - return; + const client = clientsMap.get(clientId); + if (!client?.client) { + throw new Error(`Client ${clientId} not found`); } - - logger.info(`Executing MCP request for ${clientId}`); - - // Execute request and return result - return await executeRequest(client, request); + logger.info(`Executing request for [${clientId}]`); + return await executeRequest(client.client, request); } catch (error) { - logger.error(`MCP execution error: ${error}`); + logger.error(`Failed to execute request for [${clientId}]: ${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) { +// 获取 MCP 配置文件 +export async function getMcpConfigFromFile(): Promise { 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 configStr = await fs.readFile(CONFIG_PATH, "utf-8"); + return JSON.parse(configStr); } 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 -> { - const status: Record = {}; - for (const [clientId, data] of clientsMap.entries()) { - status[clientId] = data.errorMsg; + logger.error(`Failed to load MCP config, using default config: ${error}`); + return DEFAULT_MCP_CONFIG; } - return status; } -// 检查客户端状态 -export async function getClientErrors(): Promise< - Record -> { - const errors: Record = {}; - for (const [clientId, data] of clientsMap.entries()) { - errors[clientId] = data.errorMsg; +// 更新 MCP 配置文件 +async function updateMcpConfig(config: McpConfigData): Promise { + try { + await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2)); + } catch (error) { + throw error; } - return errors; } -// 获取客户端状态,不重新初始化 -export async function refreshClientStatus() { - logger.info("Refreshing client status..."); - - // 如果还没初始化过,则初始化 - if (!initialized) { - return initializeMcpClients(); +// 重新初始化单个客户端 +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}`); } - - // 否则只更新错误状态 - errorClients = []; - for (const [clientId, clientData] of clientsMap.entries()) { - if (clientData.errorMsg !== null) { - errorClients.push(clientId); - } - } - - return { errorClients }; + await initializeSingleClient(clientId, serverConfig); } diff --git a/app/mcp/client.ts b/app/mcp/client.ts index 6650f9e2b42..b7b511a9232 100644 --- a/app/mcp/client.ts +++ b/app/mcp/client.ts @@ -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; -} - const logger = new MCPClientLogger(); export async function createClient( - serverConfig: ServerConfig, - name: string, + id: string, + config: ServerConfig, ): Promise { - 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 { - 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 { + return client.listTools(); } -/** Execute a request */ export async function executeRequest( client: Client, request: McpRequestMessage, diff --git a/app/mcp/example.ts b/app/mcp/example.ts index f3b91fb8cbd..986196d632c 100644 --- a/app/mcp/example.ts +++ b/app/mcp/example.ts @@ -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)}`, ); } diff --git a/app/mcp/mcp_config.json b/app/mcp/mcp_config.json index da39e4ffafe..8a235acc9ce 100644 --- a/app/mcp/mcp_config.json +++ b/app/mcp/mcp_config.json @@ -1,3 +1,12 @@ { - "mcpServers": {} -} + "mcpServers": { + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "." + ] + } + } +} \ No newline at end of file diff --git a/app/mcp/preset-server.json b/app/mcp/preset-server.json index 0daec9aebf1..b44b841d290 100644 --- a/app/mcp/preset-server.json +++ b/app/mcp/preset-server.json @@ -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 diff --git a/app/mcp/types.ts b/app/mcp/types.ts index a97c94e059a..da6731d284f 100644 --- a/app/mcp/types.ts +++ b/app/mcp/types.ts @@ -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 = 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; } -export interface McpConfig { +export interface McpConfigData { + // MCP Server 的配置 mcpServers: Record; } +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; } diff --git a/app/page.tsx b/app/page.tsx index d4ba2a27613..48a70220190 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"; +import { initializeMcpSystem } from "./mcp/actions"; const serverConfig = getServerSideConfig(); export default async function App() { - await initializeMcpClients(); + // 初始化 MCP 系统 + await initializeMcpSystem(); return ( <> diff --git a/app/store/chat.ts b/app/store/chat.ts index 4a70c9296c3..6c6c70a1c9f 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -21,8 +21,8 @@ import { DEFAULT_SYSTEM_TEMPLATE, GEMINI_SUMMARIZE_MODEL, KnowledgeCutOffDate, - MCP_PRIMITIVES_TEMPLATE, MCP_SYSTEM_TEMPLATE, + MCP_TOOLS_TEMPLATE, ServiceProvider, StoreKey, SUMMARIZE_MODEL, @@ -35,7 +35,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config"; import { useAccessStore } from "./access"; import { collectModelsWithDefaultModel } from "../utils/model"; import { createEmptyMask, Mask } from "./mask"; -import { executeMcpAction, getAllPrimitives } from "../mcp/actions"; +import { executeMcpAction, getAllTools } from "../mcp/actions"; import { extractMcpJson, isMcpJson } from "../mcp/utils"; const localStorage = safeLocalStorage(); @@ -199,23 +199,24 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) { } async function getMcpSystemPrompt(): Promise { - let primitives = await getAllPrimitives(); - primitives = primitives.filter((i) => - i.primitives.some((p) => p.type === "tool"), - ); + const tools = await getAllTools(); + + let toolsStr = ""; + + tools.forEach((i) => { + // error client has no tools + if (!i.tools) return; - let primitivesString = ""; - primitives.forEach((i) => { - primitivesString += MCP_PRIMITIVES_TEMPLATE.replace( + toolsStr += MCP_TOOLS_TEMPLATE.replace( "{{ clientId }}", i.clientId, ).replace( - "{{ primitives }}", - i.primitives.map((p) => JSON.stringify(p, null, 2)).join("\n"), + "{{ tools }}", + i.tools.tools.map((p: object) => JSON.stringify(p, null, 2)).join("\n"), ); }); - return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_PRIMITIVES }}", primitivesString); + return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_TOOLS }}", toolsStr); } const DEFAULT_CHAT_STATE = { From a70e9a3c01dccb887fc41c3d60f2c101d0b1cf2e Mon Sep 17 00:00:00 2001 From: river Date: Wed, 15 Jan 2025 17:23:10 +0800 Subject: [PATCH 16/29] =?UTF-8?q?chore=EF=BC=9Aupdate=20mcp=20icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/mcp/mcp_config.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 app/mcp/mcp_config.json diff --git a/app/mcp/mcp_config.json b/app/mcp/mcp_config.json deleted file mode 100644 index 8a235acc9ce..00000000000 --- a/app/mcp/mcp_config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "filesystem": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-filesystem", - "." - ] - } - } -} \ No newline at end of file From be59de56f0074c4fde7358465f844d09b48ab273 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Wed, 15 Jan 2025 17:24:04 +0800 Subject: [PATCH 17/29] feat: Display the number of clients instead of the number of available tools. --- app/components/mcp-market.tsx | 16 ++-------------- app/mcp/actions.ts | 6 +----- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index d93754549e3..fc088c03b94 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -61,16 +61,6 @@ export function McpMarketPage() { return id in (config?.mcpServers ?? {}); }; - // 获取客户端状态 - const updateClientStatus = async (clientId: string) => { - const status = await getClientStatus(clientId); - setClientStatuses((prev) => ({ - ...prev, - [clientId]: status, - })); - return status; - }; - // 从服务器获取初始状态 useEffect(() => { const loadInitialState = async () => { @@ -82,8 +72,7 @@ export function McpMarketPage() { // 获取所有客户端的状态 const statuses: Record = {}; for (const clientId of Object.keys(config.mcpServers)) { - const status = await getClientStatus(clientId); - statuses[clientId] = status; + statuses[clientId] = await getClientStatus(clientId); } setClientStatuses(statuses); } catch (error) { @@ -220,8 +209,7 @@ export function McpMarketPage() { // 更新所有客户端状态 const statuses: Record = {}; for (const clientId of Object.keys(newConfig.mcpServers)) { - const status = await getClientStatus(clientId); - statuses[clientId] = status; + statuses[clientId] = await getClientStatus(clientId); } setClientStatuses(statuses); diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index 6b5ea6df358..c6b9fd75f94 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -40,11 +40,7 @@ export async function getClientTools(clientId: string) { // 获取可用客户端数量 export async function getAvailableClientsCount() { let count = 0; - clientsMap.forEach((map) => { - if (!map.errorMsg) { - count += map?.tools?.tools?.length ?? 0; - } - }); + clientsMap.forEach((map) => !map.errorMsg && count++); return count; } From c89e4883b29142cfcb9254b7ff9815a5fe0b8d67 Mon Sep 17 00:00:00 2001 From: river Date: Wed, 15 Jan 2025 17:31:18 +0800 Subject: [PATCH 18/29] chore: update icon --- app/icons/tool.svg | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/icons/tool.svg b/app/icons/tool.svg index f7543e2013d..add538457ce 100644 --- a/app/icons/tool.svg +++ b/app/icons/tool.svg @@ -1,5 +1 @@ - - - \ No newline at end of file + \ No newline at end of file From e440ff56c89d11b29cdbb303eb8a9a71cddc2553 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Wed, 15 Jan 2025 18:47:05 +0800 Subject: [PATCH 19/29] fix: env not work --- app/mcp/client.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/mcp/client.ts b/app/mcp/client.ts index b7b511a9232..5c2f071e301 100644 --- a/app/mcp/client.ts +++ b/app/mcp/client.ts @@ -15,7 +15,14 @@ export async function createClient( const transport = new StdioClientTransport({ command: config.command, args: config.args, - env: config.env, + env: { + ...Object.fromEntries( + Object.entries(process.env) + .filter(([_, v]) => v !== undefined) + .map(([k, v]) => [k, v as string]), + ), + ...(config.env || {}), + }, }); const client = new Client( From 07c63497dcbacee489d24db890281f84c2793e78 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 16 Jan 2025 08:52:54 +0800 Subject: [PATCH 20/29] feat: support stop/start MCP servers --- app/components/mcp-market.module.scss | 40 +---- app/components/mcp-market.tsx | 210 ++++++++++++++++++-------- app/icons/pause.svg | 4 +- app/icons/play.svg | 3 + app/mcp/actions.ts | 139 ++++++++++++++++- app/mcp/preset-server.json | 25 --- app/mcp/types.ts | 9 ++ 7 files changed, 298 insertions(+), 132 deletions(-) create mode 100644 app/icons/play.svg diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss index 93c6b67de6a..a3025c03ec9 100644 --- a/app/components/mcp-market.module.scss +++ b/app/components/mcp-market.module.scss @@ -98,6 +98,10 @@ background-color: #ef4444; } + &.stopped { + background-color: #6b7280; + } + .error-message { margin-left: 4px; font-size: 12px; @@ -151,21 +155,11 @@ .mcp-market-actions { display: flex; - gap: 8px; + gap: 12px; align-items: flex-start; flex-shrink: 0; min-width: 180px; justify-content: flex-end; - - :global(.icon-button) { - transition: all 0.3s ease; - border: 1px solid transparent; - - &:hover { - transform: translateY(-1px); - filter: brightness(1.1); - } - } } } } @@ -213,30 +207,6 @@ color: var(--gray-300); } } - - :global(.icon-button) { - width: 32px; - height: 32px; - padding: 0; - border-radius: 6px; - background-color: transparent; - border: 1px solid var(--gray-200); - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - background-color: var(--gray-100); - border-color: var(--gray-300); - } - - svg { - width: 16px; - height: 16px; - opacity: 0.7; - } - } } :global(.icon-button.add-path-button) { diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index fc088c03b94..0e46e776685 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -17,16 +17,20 @@ import { getClientStatus, getClientTools, getMcpConfigFromFile, - removeMcpServer, restartAllClients, + pauseMcpServer, + resumeMcpServer, } from "../mcp/actions"; import { ListToolsResponse, McpConfigData, PresetServer, ServerConfig, + ServerStatusResponse, } from "../mcp/types"; import clsx from "clsx"; +import PlayIcon from "../icons/play.svg"; +import StopIcon from "../icons/pause.svg"; const presetServers = presetServersJson as PresetServer[]; @@ -47,13 +51,7 @@ export function McpMarketPage() { const [isLoading, setIsLoading] = useState(false); const [config, setConfig] = useState(); const [clientStatuses, setClientStatuses] = useState< - Record< - string, - { - status: "active" | "error" | "undefined"; - errorMsg: string | null; - } - > + Record >({}); // 检查服务器是否已添加 @@ -253,18 +251,74 @@ export function McpMarketPage() { }; // 移除服务器 - const removeServer = async (id: string) => { + // const removeServer = async (id: string) => { + // try { + // setIsLoading(true); + // const newConfig = await removeMcpServer(id); + // setConfig(newConfig); + + // // 移除状态 + // setClientStatuses((prev) => { + // const newStatuses = { ...prev }; + // delete newStatuses[id]; + // return newStatuses; + // }); + // } finally { + // setIsLoading(false); + // } + // }; + + // 暂停服务器 + const pauseServer = async (id: string) => { try { setIsLoading(true); - const newConfig = await removeMcpServer(id); + showToast("Stopping server..."); + const newConfig = await pauseMcpServer(id); setConfig(newConfig); - // 移除状态 - setClientStatuses((prev) => { - const newStatuses = { ...prev }; - delete newStatuses[id]; - return newStatuses; - }); + // 更新状态为暂停 + setClientStatuses((prev) => ({ + ...prev, + [id]: { status: "paused", errorMsg: null }, + })); + showToast("Server stopped successfully"); + } catch (error) { + showToast("Failed to stop server"); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + // 恢复服务器 + const resumeServer = async (id: string) => { + try { + setIsLoading(true); + showToast("Starting server..."); + + // 尝试启动服务器 + const success = await resumeMcpServer(id); + + // 获取最新状态(这个状态是从 clientsMap 中获取的,反映真实状态) + const status = await getClientStatus(id); + setClientStatuses((prev) => ({ + ...prev, + [id]: status, + })); + + // 根据启动结果显示消息 + if (success) { + showToast("Server started successfully"); + } else { + throw new Error("Failed to start server"); + } + } catch (error) { + showToast( + error instanceof Error + ? error.message + : "Failed to start server, please check logs", + ); + console.error(error); } finally { setIsLoading(false); } @@ -332,7 +386,12 @@ export function McpMarketPage() { } else if (prop.type === "string") { const currentValue = userConfig[key as keyof typeof userConfig] || ""; return ( - +
{ + const status = checkServerStatus(clientId); + + const statusMap = { + undefined: null, // 未配置/未找到不显示 + paused: ( + + Stopped + + ), + active: Running, + error: ( + + Error + : {status.errorMsg} + + ), + }; + + return statusMap[status.status]; + }; + // 渲染服务器列表 const renderServerList = () => { return presetServers @@ -373,15 +455,18 @@ export function McpMarketPage() { const bStatus = checkServerStatus(b.id).status; // 定义状态优先级 - const statusPriority = { - error: 0, - active: 1, - undefined: 2, + const statusPriority: Record = { + error: 0, // 最高优先级 + active: 1, // 运行中 + paused: 2, // 已暂停 + undefined: 3, // 未配置/未找到 }; // 首先按状态排序 if (aStatus !== bStatus) { - return statusPriority[aStatus] - statusPriority[bStatus]; + return ( + (statusPriority[aStatus] || 3) - (statusPriority[bStatus] || 3) + ); } // 然后按名称排序 @@ -398,25 +483,7 @@ export function McpMarketPage() {
{server.name} - {checkServerStatus(server.id).status !== "undefined" && ( - - {checkServerStatus(server.id).status === "error" ? ( - <> - Error - - : {checkServerStatus(server.id).errorMsg} - - - ) : ( - "Active" - )} - - )} + {getServerStatusDisplay(server.id)} {server.repo && ( } text="Configure" - className={clsx({ - [styles["action-error"]]: - checkServerStatus(server.id).status === "error", - })} onClick={() => setEditingServerId(server.id)} disabled={isLoading} /> )} - } - text="Tools" - onClick={async () => { - setViewingServerId(server.id); - await loadTools(server.id); - }} - disabled={ - isLoading || - checkServerStatus(server.id).status === "error" - } - /> - } - text="Remove" - className={styles["action-danger"]} - onClick={() => removeServer(server.id)} - disabled={isLoading} - /> + {checkServerStatus(server.id).status === "paused" ? ( + <> + } + text="Start" + onClick={() => resumeServer(server.id)} + disabled={isLoading} + /> + {/* } + text="Remove" + onClick={() => removeServer(server.id)} + disabled={isLoading} + /> */} + + ) : ( + <> + } + text="Tools" + onClick={async () => { + setViewingServerId(server.id); + await loadTools(server.id); + }} + disabled={ + isLoading || + checkServerStatus(server.id).status === "error" + } + /> + } + text="Stop" + onClick={() => pauseServer(server.id)} + disabled={isLoading} + /> + + )} ) : ( } text="Add" - className={styles["action-primary"]} onClick={() => addServer(server)} disabled={isLoading} /> diff --git a/app/icons/pause.svg b/app/icons/pause.svg index 4e81ef06732..08a6572d6cf 100644 --- a/app/icons/pause.svg +++ b/app/icons/pause.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/icons/play.svg b/app/icons/play.svg new file mode 100644 index 00000000000..4a2515c6f1e --- /dev/null +++ b/app/icons/play.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index c6b9fd75f94..ba1525be7d4 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -12,6 +12,7 @@ import { McpConfigData, McpRequestMessage, ServerConfig, + ServerStatusResponse, } from "./types"; import fs from "fs/promises"; import path from "path"; @@ -22,14 +23,40 @@ const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json"); const clientsMap = new Map(); // 获取客户端状态 -export async function getClientStatus(clientId: string) { +export async function getClientStatus( + clientId: string, +): Promise { const status = clientsMap.get(clientId); - if (!status) return { status: "undefined" as const, errorMsg: null }; + const config = await getMcpConfigFromFile(); + const serverConfig = config.mcpServers[clientId]; + + // 如果配置中不存在该服务器 + if (!serverConfig) { + return { status: "undefined", errorMsg: null }; + } + + // 如果服务器配置为暂停状态 + if (serverConfig.status === "paused") { + return { status: "paused", errorMsg: null }; + } + + // 如果 clientsMap 中没有记录 + if (!status) { + return { status: "undefined", errorMsg: null }; + } + + // 如果有错误 + if (status.errorMsg) { + return { status: "error", errorMsg: status.errorMsg }; + } + + // 如果客户端正常运行 + if (status.client) { + return { status: "active", errorMsg: null }; + } - return { - status: status.errorMsg ? ("error" as const) : ("active" as const), - errorMsg: status.errorMsg, - }; + // 如果客户端不存在 + return { status: "error", errorMsg: "Client not found" }; } // 获取客户端工具 @@ -61,6 +88,12 @@ 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}]...`); try { const client = await createClient(clientId, serverConfig); @@ -114,6 +147,100 @@ export async function addMcpServer(clientId: string, config: ServerConfig) { } } +// 暂停服务器 +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" as const, + }, + }, + }; + 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 { + 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); + + // 再次确认状态 + const status = await getClientStatus(clientId); + return status.status === "active"; + } 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}`); + return false; + } + } catch (error) { + logger.error(`Failed to resume server [${clientId}]: ${error}`); + throw error; + } +} + // 移除服务器 export async function removeMcpServer(clientId: string) { try { diff --git a/app/mcp/preset-server.json b/app/mcp/preset-server.json index b44b841d290..84fe234bdf4 100644 --- a/app/mcp/preset-server.json +++ b/app/mcp/preset-server.json @@ -72,31 +72,6 @@ "baseArgs": ["-y", "@executeautomation/playwright-mcp-server"], "configurable": false }, - { - "id": "mongodb", - "name": "MongoDB", - "description": "Direct interaction with MongoDB databases", - "repo": "", - "tags": ["database", "mongodb", "nosql"], - "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", diff --git a/app/mcp/types.ts b/app/mcp/types.ts index da6731d284f..85e94f3b8a6 100644 --- a/app/mcp/types.ts +++ b/app/mcp/types.ts @@ -87,11 +87,20 @@ interface McpErrorClient { errorMsg: string; } +// 服务器状态类型 +export type ServerStatus = "undefined" | "active" | "paused" | "error"; + +export interface ServerStatusResponse { + status: ServerStatus; + errorMsg: string | null; +} + // MCP 服务器配置相关类型 export interface ServerConfig { command: string; args: string[]; env?: Record; + status?: "active" | "paused" | "error"; } export interface McpConfigData { From 4d63d73b2e8b7b382a4cc1f60fdd20cb8c5f953a Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 16 Jan 2025 09:00:57 +0800 Subject: [PATCH 21/29] feat: load MCP preset data from server --- app/components/mcp-market.module.scss | 21 +++++++++++++ app/components/mcp-market.tsx | 43 +++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss index a3025c03ec9..46f3c336863 100644 --- a/app/components/mcp-market.module.scss +++ b/app/components/mcp-market.module.scss @@ -17,6 +17,27 @@ padding: 20px; overflow-y: auto; + .loading-container, + .empty-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; + width: 100%; + background-color: var(--white); + border: var(--border-in-light); + border-radius: 10px; + animation: slide-in ease 0.3s; + } + + .loading-text, + .empty-text { + font-size: 14px; + color: var(--black); + opacity: 0.5; + text-align: center; + } + .mcp-market-filter { width: 100%; max-width: 100%; diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index 0e46e776685..bbf0d4d45dd 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -11,7 +11,6 @@ import GithubIcon from "../icons/github.svg"; import { List, ListItem, Modal, showToast } from "./ui-lib"; import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; -import presetServersJson from "../mcp/preset-server.json"; import { addMcpServer, getClientStatus, @@ -32,8 +31,6 @@ import clsx from "clsx"; import PlayIcon from "../icons/play.svg"; import StopIcon from "../icons/pause.svg"; -const presetServers = presetServersJson as PresetServer[]; - interface ConfigProperty { type: string; description?: string; @@ -53,6 +50,28 @@ export function McpMarketPage() { const [clientStatuses, setClientStatuses] = useState< Record >({}); + const [loadingPresets, setLoadingPresets] = useState(true); + const [presetServers, setPresetServers] = useState([]); + + useEffect(() => { + const loadPresetServers = async () => { + try { + setLoadingPresets(true); + const response = await fetch("https://nextchat.club/mcp/list"); + if (!response.ok) { + throw new Error("Failed to load preset servers"); + } + const data = await response.json(); + setPresetServers(data?.data ?? []); + } catch (error) { + console.error("Failed to load preset servers:", error); + showToast("Failed to load preset servers"); + } finally { + setLoadingPresets(false); + } + }; + loadPresetServers().then(); + }, []); // 检查服务器是否已添加 const isServerAdded = (id: string) => { @@ -440,6 +459,24 @@ export function McpMarketPage() { // 渲染服务器列表 const renderServerList = () => { + if (loadingPresets) { + return ( +
+
+ Loading preset server list... +
+
+ ); + } + + if (!Array.isArray(presetServers) || presetServers.length === 0) { + return ( +
+
No servers available
+
+ ); + } + return presetServers .filter((server) => { if (searchText.length === 0) return true; From d4f499ee41c8ab1c044fb690b980dc3d903d4e25 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 16 Jan 2025 09:11:53 +0800 Subject: [PATCH 22/29] feat: adjust form style --- app/components/mcp-market.tsx | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index bbf0d4d45dd..0bd4a7dd6aa 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -140,7 +140,7 @@ export function McpMarketPage() { setUserConfig({}); } } - }, [editingServerId, config]); + }, [editingServerId, config, presetServers]); // 保存服务器配置 const saveServerConfig = async () => { @@ -405,22 +405,16 @@ export function McpMarketPage() { } else if (prop.type === "string") { const currentValue = userConfig[key as keyof typeof userConfig] || ""; return ( - -
- { - setUserConfig({ ...userConfig, [key]: e.target.value }); - }} - /> -
+ + { + setUserConfig({ ...userConfig, [key]: e.target.value }); + }} + /> ); } From 588d81e8f19047110a87196259df9fc2e8dbc0ce Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 16 Jan 2025 09:17:08 +0800 Subject: [PATCH 23/29] feat: remove unused files --- app/components/mcp-market.tsx | 6 - app/mcp/example.ts | 27 ----- app/mcp/preset-server.json | 203 ---------------------------------- 3 files changed, 236 deletions(-) delete mode 100644 app/mcp/example.ts delete mode 100644 app/mcp/preset-server.json diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index 0bd4a7dd6aa..9aff190b898 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -102,12 +102,6 @@ export function McpMarketPage() { loadInitialState(); }, []); - // Debug: 监控状态变化 - useEffect(() => { - console.log("MCP Market - Current config:", config); - console.log("MCP Market - Current clientStatuses:", clientStatuses); - }, [config, clientStatuses]); - // 加载当前编辑服务器的配置 useEffect(() => { if (editingServerId && config) { diff --git a/app/mcp/example.ts b/app/mcp/example.ts deleted file mode 100644 index 986196d632c..00000000000 --- a/app/mcp/example.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 = "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(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(tools, null, 2)}`, - ); -} - -main().catch((error) => { - logger.error(error); - process.exit(1); -}); diff --git a/app/mcp/preset-server.json b/app/mcp/preset-server.json deleted file mode 100644 index 84fe234bdf4..00000000000 --- a/app/mcp/preset-server.json +++ /dev/null @@ -1,203 +0,0 @@ -[ - { - "id": "filesystem", - "name": "Filesystem", - "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, - "configSchema": { - "properties": { - "paths": { - "type": "array", - "description": "Allowed file system paths", - "required": true, - "minItems": 1, - "itemLabel": "Path", - "addButtonText": "Add Path" - } - } - }, - "argsMapping": { - "paths": { - "type": "spread", - "position": 2 - } - } - }, - { - "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, - "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", - "repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/gdrive", - "tags": ["google", "drive", "storage", "cloud"], - "command": "npx", - "baseArgs": ["-y", "@modelcontextprotocol/server-gdrive"], - "configurable": false - }, - { - "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 - }, - { - "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, - "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", - "repo": "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres", - "tags": ["database", "postgresql", "sql"], - "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", - "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, - "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", - "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, - "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", - "repo": "https://github.com/QuantGeekDev/docker-mcp", - "tags": ["docker", "container", "devops"], - "command": "uvx", - "baseArgs": ["docker-mcp"], - "configurable": false - } -] From 4d535b1cd0c641d573a97e03fb5d9cb84a9f5ce5 Mon Sep 17 00:00:00 2001 From: river Date: Thu, 16 Jan 2025 20:54:24 +0800 Subject: [PATCH 24/29] chore: enhance mcp prompt --- app/constant.ts | 81 ++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/app/constant.ts b/app/constant.ts index 9cdf197bfba..ed244068e99 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -281,7 +281,7 @@ You are an AI assistant with access to system tools. Your role is to help users A. Tool Call Format: - Use markdown code blocks with format: \`\`\`json:mcp:{clientId}\`\`\` - Always include: - * method: "tools/call" + * method: "tools/call"(Only this method is supported) * params: - name: must match an available primitive name - arguments: required parameters for the primitive @@ -292,6 +292,7 @@ You are an AI assistant with access to system tools. Your role is to help users - Wait for response before making another tool call C. Important Rules: + - Only use tools/call method - Only ONE tool call per message - ALWAYS TAKE ACTION instead of just describing what you could do - Include the correct clientId in code block language tag @@ -310,8 +311,9 @@ You are an AI assistant with access to system tools. Your role is to help users - Try alternative approach immediately 5. EXAMPLE INTERACTION: - User: "What files do I have on my desktop?" - Assistant: "I'll check which directories I have access to. + + good example: + \`\`\`json:mcp:filesystem { "method": "tools/call", @@ -322,48 +324,59 @@ You are an AI assistant with access to system tools. Your role is to help users } \`\`\`" - User: "\`\`\`json:mcp-response:filesystem + + \`\`\`json:mcp-response:filesystem + { + "method": "tools/call", + "params": { + "name": "write_file", + "arguments": { + "path": "/Users/river/dev/nextchat/test/joke.txt", + "content": "为什么数学书总是感到忧伤?因为它有太多的问题。" + } + } + } +\`\`\` + + follwing is the wrong! mcp json example: + + \`\`\`json:mcp:filesystem { - "directories": ["/path/to/desktop"] + "method": "write_file", + "params": { + "path": "NextChat_Information.txt", + "content": "1" + } } - \`\`\`" + \`\`\` + + This is wrong because the method is not tools/call. + + \`\`\`{ + "method": "search_repositories", + "params": { + "query": "2oeee" + } +} + \`\`\` + + This is wrong because the method is not tools/call.!!!!!!!!!!! - Assistant: "I can see that I have access to your desktop directory. Let me list its contents for you. + the right format is: \`\`\`json:mcp:filesystem { "method": "tools/call", "params": { - "name": "list_directory", + "name": "search_repositories", "arguments": { - "path": "/path/to/desktop" + "query": "2oeee" } } } - \`\`\`" - - User: "\`\`\`json:mcp-response:filesystem - { - "content": [ - { - "type": "text", - "text": "[FILE] document.txt\n[DIR] folder1\n[DIR] folder2\n[FILE] image.png\n[FILE] notes.md" - } - ] - } - \`\`\`" - - Assistant: "I've found the contents of your desktop. Here's what you have: - - Files: - - document.txt - - image.png - - notes.md - - Directories: - - folder1 - - folder2 - - Would you like to explore any of these directories or perform other operations with these files?" + \`\`\` + + please follow the format strictly ONLY use tools/call method!!!!!!!!!!! + `; export const SUMMARIZE_MODEL = "gpt-4o-mini"; From 65810d918bb599716e35c8ea515a265da909cf2f Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 16 Jan 2025 21:30:15 +0800 Subject: [PATCH 25/29] feat: improve async operations and UI feedback --- app/components/mcp-market.module.scss | 65 +++++++++ app/components/mcp-market.tsx | 198 ++++++++++++++++---------- app/mcp/actions.ts | 18 ++- 3 files changed, 201 insertions(+), 80 deletions(-) diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss index 46f3c336863..f5c8c0ccae3 100644 --- a/app/components/mcp-market.module.scss +++ b/app/components/mcp-market.module.scss @@ -85,6 +85,50 @@ border-bottom-right-radius: 10px; } + &.loading { + position: relative; + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.2), + transparent + ); + background-size: 200% 100%; + animation: loading-pulse 1.5s infinite; + } + } + + .operation-status { + display: inline-flex; + align-items: center; + margin-left: 10px; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + background-color: #16a34a; + color: #fff; + animation: pulse 1.5s infinite; + + &[data-status="stopping"] { + background-color: #9ca3af; + } + + &[data-status="starting"] { + background-color: #4ade80; + } + + &[data-status="error"] { + background-color: #f87171; + } + } + .mcp-market-header { display: flex; justify-content: space-between; @@ -585,3 +629,24 @@ } } } + +@keyframes loading-pulse { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +@keyframes pulse { + 0% { + opacity: 0.6; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.6; + } +} diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index 9aff190b898..a7cea879de7 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -52,6 +52,9 @@ export function McpMarketPage() { >({}); const [loadingPresets, setLoadingPresets] = useState(true); const [presetServers, setPresetServers] = useState([]); + const [loadingStates, setLoadingStates] = useState>( + {}, + ); useEffect(() => { const loadPresetServers = async () => { @@ -141,8 +144,12 @@ export function McpMarketPage() { const preset = presetServers.find((s) => s.id === editingServerId); if (!preset || !preset.configSchema || !editingServerId) return; + // 先关闭模态框 + const savingServerId = editingServerId; + setEditingServerId(undefined); + try { - setIsLoading(true); + updateLoadingState(savingServerId, "Updating configuration..."); // 构建服务器配置 const args = [...preset.baseArgs]; const env: Record = {}; @@ -172,25 +179,38 @@ export function McpMarketPage() { ...(Object.keys(env).length > 0 ? { env } : {}), }; + // 检查是否是新增还是编辑 + const isNewServer = !isServerAdded(savingServerId); + + // 如果是编辑现有服务器,保持原有状态 + if (!isNewServer) { + const currentConfig = await getMcpConfigFromFile(); + const currentStatus = currentConfig.mcpServers[savingServerId]?.status; + if (currentStatus) { + serverConfig.status = currentStatus; + } + } + // 更新配置并初始化新服务器 - const newConfig = await addMcpServer(editingServerId, serverConfig); + const newConfig = await addMcpServer(savingServerId, serverConfig); setConfig(newConfig); - // 更新状态 - const status = await getClientStatus(editingServerId); - setClientStatuses((prev) => ({ - ...prev, - [editingServerId]: status, - })); + // 只有新增的服务器才需要获取状态(因为会自动启动) + if (isNewServer) { + const status = await getClientStatus(savingServerId); + setClientStatuses((prev) => ({ + ...prev, + [savingServerId]: status, + })); + } - setEditingServerId(undefined); - showToast("Server configuration saved successfully"); + showToast("Server configuration updated successfully"); } catch (error) { showToast( error instanceof Error ? error.message : "Failed to save configuration", ); } finally { - setIsLoading(false); + updateLoadingState(savingServerId, null); } }; @@ -210,36 +230,24 @@ export function McpMarketPage() { } }; - // 重启所有客户端 - const handleRestartAll = async () => { - try { - setIsLoading(true); - const newConfig = await restartAllClients(); - setConfig(newConfig); - - // 更新所有客户端状态 - const statuses: Record = {}; - for (const clientId of Object.keys(newConfig.mcpServers)) { - statuses[clientId] = await getClientStatus(clientId); + // 更新加载状态的辅助函数 + const updateLoadingState = (id: string, message: string | null) => { + setLoadingStates((prev) => { + if (message === null) { + const { [id]: _, ...rest } = prev; + return rest; } - setClientStatuses(statuses); - - showToast("Successfully restarted all clients"); - } catch (error) { - showToast("Failed to restart clients"); - console.error(error); - } finally { - setIsLoading(false); - } + return { ...prev, [id]: message }; + }); }; - // 添加服务器 + // 修改添加服务器函数 const addServer = async (preset: PresetServer) => { if (!preset.configurable) { try { - setIsLoading(true); - showToast("Creating MCP client..."); - // 如果服务器不需要配置,直接添加 + const serverId = preset.id; + updateLoadingState(serverId, "Creating MCP client..."); + const serverConfig: ServerConfig = { command: preset.command, args: [...preset.baseArgs], @@ -254,7 +262,7 @@ export function McpMarketPage() { [preset.id]: status, })); } finally { - setIsLoading(false); + updateLoadingState(preset.id, null); } } else { // 如果需要配置,打开配置对话框 @@ -263,33 +271,13 @@ export function McpMarketPage() { } }; - // 移除服务器 - // const removeServer = async (id: string) => { - // try { - // setIsLoading(true); - // const newConfig = await removeMcpServer(id); - // setConfig(newConfig); - - // // 移除状态 - // setClientStatuses((prev) => { - // const newStatuses = { ...prev }; - // delete newStatuses[id]; - // return newStatuses; - // }); - // } finally { - // setIsLoading(false); - // } - // }; - - // 暂停服务器 + // 修改暂停服务器函数 const pauseServer = async (id: string) => { try { - setIsLoading(true); - showToast("Stopping server..."); + updateLoadingState(id, "Stopping server..."); const newConfig = await pauseMcpServer(id); setConfig(newConfig); - // 更新状态为暂停 setClientStatuses((prev) => ({ ...prev, [id]: { status: "paused", errorMsg: null }, @@ -299,27 +287,22 @@ export function McpMarketPage() { showToast("Failed to stop server"); console.error(error); } finally { - setIsLoading(false); + updateLoadingState(id, null); } }; - // 恢复服务器 + // 修改恢复服务器函数 const resumeServer = async (id: string) => { try { - setIsLoading(true); - showToast("Starting server..."); + updateLoadingState(id, "Starting server..."); - // 尝试启动服务器 const success = await resumeMcpServer(id); - - // 获取最新状态(这个状态是从 clientsMap 中获取的,反映真实状态) const status = await getClientStatus(id); setClientStatuses((prev) => ({ ...prev, [id]: status, })); - // 根据启动结果显示消息 if (success) { showToast("Server started successfully"); } else { @@ -333,7 +316,29 @@ export function McpMarketPage() { ); console.error(error); } finally { - setIsLoading(false); + updateLoadingState(id, null); + } + }; + + // 修改重启所有客户端函数 + const handleRestartAll = async () => { + try { + updateLoadingState("all", "Restarting all servers..."); + const newConfig = await restartAllClients(); + setConfig(newConfig); + + const statuses: Record = {}; + for (const clientId of Object.keys(newConfig.mcpServers)) { + statuses[clientId] = await getClientStatus(clientId); + } + setClientStatuses(statuses); + + showToast("Successfully restarted all clients"); + } catch (error) { + showToast("Failed to restart clients"); + console.error(error); + } finally { + updateLoadingState("all", null); } }; @@ -445,6 +450,14 @@ export function McpMarketPage() { return statusMap[status.status]; }; + // 获取操作状态的类型 + const getOperationStatusType = (message: string) => { + if (message.toLowerCase().includes("stopping")) return "stopping"; + if (message.toLowerCase().includes("starting")) return "starting"; + if (message.toLowerCase().includes("error")) return "error"; + return "default"; + }; + // 渲染服务器列表 const renderServerList = () => { if (loadingPresets) { @@ -478,29 +491,46 @@ export function McpMarketPage() { .sort((a, b) => { const aStatus = checkServerStatus(a.id).status; const bStatus = checkServerStatus(b.id).status; + const aLoading = loadingStates[a.id]; + const bLoading = loadingStates[b.id]; // 定义状态优先级 const statusPriority: Record = { - error: 0, // 最高优先级 - active: 1, // 运行中 - paused: 2, // 已暂停 - undefined: 3, // 未配置/未找到 + error: 0, // 错误状态最高优先级 + active: 1, // 已启动次之 + starting: 2, // 正在启动 + stopping: 3, // 正在停止 + paused: 4, // 已暂停 + undefined: 5, // 未配置最低优先级 + }; + + // 获取实际状态(包括加载状态) + const getEffectiveStatus = (status: string, loading?: string) => { + if (loading) { + const operationType = getOperationStatusType(loading); + return operationType === "default" ? status : operationType; + } + return status; }; + const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading); + const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading); + // 首先按状态排序 - if (aStatus !== bStatus) { + if (aEffectiveStatus !== bEffectiveStatus) { return ( - (statusPriority[aStatus] || 3) - (statusPriority[bStatus] || 3) + (statusPriority[aEffectiveStatus] ?? 5) - + (statusPriority[bEffectiveStatus] ?? 5) ); } - // 然后按名称排序 + // 状态相同时按名称排序 return a.name.localeCompare(b.name); }) .map((server) => (
@@ -508,7 +538,17 @@ export function McpMarketPage() {
{server.name} - {getServerStatusDisplay(server.id)} + {loadingStates[server.id] && ( + + {loadingStates[server.id]} + + )} + {!loadingStates[server.id] && getServerStatusDisplay(server.id)} {server.repo && (
MCP Market - {isLoading && ( - Loading... + {loadingStates["all"] && ( + + {loadingStates["all"]} + )}
diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index ba1525be7d4..2248d1327c6 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -98,6 +98,9 @@ async function initializeSingleClient( try { const client = await createClient(clientId, serverConfig); 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) { @@ -130,6 +133,13 @@ export async function initializeMcpSystem() { 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: { @@ -138,8 +148,12 @@ export async function addMcpServer(clientId: string, config: ServerConfig) { }, }; await updateMcpConfig(newConfig); - // 只初始化新添加的服务器 - await initializeSingleClient(clientId, config); + + // 只有新服务器或状态为 active 的服务器才初始化 + if (isNewServer || config.status === "active") { + await initializeSingleClient(clientId, config); + } + return newConfig; } catch (error) { logger.error(`Failed to add server [${clientId}]: ${error}`); From 0112b54bc7b0d929b6f127daf00cfb0f2e05d1bc Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Thu, 16 Jan 2025 22:35:26 +0800 Subject: [PATCH 26/29] fix: missing en translation --- app/locales/en.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/locales/en.ts b/app/locales/en.ts index fddb6f09153..6ceb425ddac 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -635,6 +635,9 @@ const en: LocaleType = { Discovery: { Name: "Discovery", }, + Mcp: { + Name: "MCP", + }, FineTuned: { Sysmessage: "You are an assistant that", }, From bc71ae247bd1110658aef933eaf301b344181122 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sat, 18 Jan 2025 21:19:01 +0800 Subject: [PATCH 27/29] feat: add ENABLE_MCP env var to toggle MCP feature globally and in Docker --- .env.template | 5 ++ Dockerfile | 4 ++ app/components/chat.tsx | 17 +++-- app/components/home.tsx | 23 +++--- app/components/mcp-market.tsx | 128 +++++++++++++++++++--------------- app/components/sidebar.tsx | 37 +++++++--- app/config/server.ts | 7 +- app/layout.tsx | 5 +- app/mcp/actions.ts | 18 +++++ app/page.tsx | 4 -- 10 files changed, 161 insertions(+), 87 deletions(-) diff --git a/.env.template b/.env.template index 82f44216ab8..c0cd80c65df 100644 --- a/.env.template +++ b/.env.template @@ -7,6 +7,11 @@ CODE=your-password # You can start service behind a proxy. (optional) PROXY_URL=http://localhost:7890 +# Enable MCP functionality (optional) +# Default: Empty (disabled) +# Set to "true" to enable MCP functionality +ENABLE_MCP= + # (optional) # Default: Empty # Google Gemini Pro API key, set if you want to use Google Gemini Pro API. diff --git a/Dockerfile b/Dockerfile index ae9a17cddbd..ff009b17848 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,12 +34,16 @@ ENV PROXY_URL="" ENV OPENAI_API_KEY="" ENV GOOGLE_API_KEY="" ENV CODE="" +ENV ENABLE_MCP="" COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/server ./.next/server +RUN mkdir -p /app/app/mcp && chmod 777 /app/app/mcp +COPY --from=builder /app/app/mcp/mcp_config.json /app/app/mcp/ + EXPOSE 3000 CMD if [ -n "$PROXY_URL" ]; then \ diff --git a/app/components/chat.tsx b/app/components/chat.tsx index c8d6886e562..435a13b76bb 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -122,7 +122,7 @@ import { isEmpty } from "lodash-es"; import { getModelProvider } from "../utils/model"; import { RealtimeChat } from "@/app/components/realtime-chat"; import clsx from "clsx"; -import { getAvailableClientsCount } from "../mcp/actions"; +import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions"; const localStorage = safeLocalStorage(); @@ -135,15 +135,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { const MCPAction = () => { const navigate = useNavigate(); const [count, setCount] = useState(0); + const [mcpEnabled, setMcpEnabled] = useState(false); useEffect(() => { - const loadCount = async () => { - const count = await getAvailableClientsCount(); - setCount(count); + const checkMcpStatus = async () => { + const enabled = await isMcpEnabled(); + setMcpEnabled(enabled); + if (enabled) { + const count = await getAvailableClientsCount(); + setCount(count); + } }; - loadCount(); + checkMcpStatus(); }, []); + if (!mcpEnabled) return null; + return ( navigate(Path.McpMarket)} diff --git a/app/components/home.tsx b/app/components/home.tsx index 8a03c50b6dc..98f759a4803 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -29,8 +29,7 @@ import { getClientConfig } from "../config/client"; import { type ClientApi, getClientApi } from "../client/api"; import { useAccessStore } from "../store"; import clsx from "clsx"; -import { initializeMcpSystem } from "../mcp/actions"; -import { showToast } from "./ui-lib"; +import { initializeMcpSystem, isMcpEnabled } from "../mcp/actions"; export function Loading(props: { noLogo?: boolean }) { return ( @@ -243,14 +242,20 @@ export function Home() { useEffect(() => { console.log("[Config] got config from build time", getClientConfig()); useAccessStore.getState().fetch(); - }, []); - useEffect(() => { - // 初始化 MCP 系统 - initializeMcpSystem().catch((error) => { - console.error("Failed to initialize MCP system:", error); - showToast("Failed to initialize MCP system"); - }); + const initMcp = async () => { + try { + const enabled = await isMcpEnabled(); + if (enabled) { + console.log("[MCP] initializing..."); + await initializeMcpSystem(); + console.log("[MCP] initialized"); + } + } catch (err) { + console.error("[MCP] failed to initialize:", err); + } + }; + initMcp(); }, []); if (!useHasHydrated()) { diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index a7cea879de7..98211ceddbf 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -16,8 +16,9 @@ import { getClientStatus, getClientTools, getMcpConfigFromFile, - restartAllClients, + isMcpEnabled, pauseMcpServer, + restartAllClients, resumeMcpServer, } from "../mcp/actions"; import { @@ -30,6 +31,7 @@ import { import clsx from "clsx"; import PlayIcon from "../icons/play.svg"; import StopIcon from "../icons/pause.svg"; +import { Path } from "../constant"; interface ConfigProperty { type: string; @@ -40,6 +42,7 @@ interface ConfigProperty { export function McpMarketPage() { const navigate = useNavigate(); + const [mcpEnabled, setMcpEnabled] = useState(false); const [searchText, setSearchText] = useState(""); const [userConfig, setUserConfig] = useState>({}); const [editingServerId, setEditingServerId] = useState(); @@ -56,8 +59,22 @@ export function McpMarketPage() { {}, ); + // 检查 MCP 是否启用 + useEffect(() => { + const checkMcpStatus = async () => { + const enabled = await isMcpEnabled(); + setMcpEnabled(enabled); + if (!enabled) { + navigate(Path.Home); + } + }; + checkMcpStatus(); + }, [navigate]); + + // 加载预设服务器 useEffect(() => { const loadPresetServers = async () => { + if (!mcpEnabled) return; try { setLoadingPresets(true); const response = await fetch("https://nextchat.club/mcp/list"); @@ -73,17 +90,13 @@ export function McpMarketPage() { setLoadingPresets(false); } }; - loadPresetServers().then(); - }, []); - - // 检查服务器是否已添加 - const isServerAdded = (id: string) => { - return id in (config?.mcpServers ?? {}); - }; + loadPresetServers(); + }, [mcpEnabled]); - // 从服务器获取初始状态 + // 加载初始状态 useEffect(() => { const loadInitialState = async () => { + if (!mcpEnabled) return; try { setIsLoading(true); const config = await getMcpConfigFromFile(); @@ -103,42 +116,50 @@ export function McpMarketPage() { } }; loadInitialState(); - }, []); + }, [mcpEnabled]); // 加载当前编辑服务器的配置 useEffect(() => { - if (editingServerId && config) { - const currentConfig = config.mcpServers[editingServerId]; - if (currentConfig) { - // 从当前配置中提取用户配置 - const preset = presetServers.find((s) => s.id === editingServerId); - if (preset?.configSchema) { - const userConfig: Record = {}; - Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => { - if (mapping.type === "spread") { - // 对于 spread 类型,从 args 中提取数组 - const startPos = mapping.position ?? 0; - userConfig[key] = currentConfig.args.slice(startPos); - } else if (mapping.type === "single") { - // 对于 single 类型,获取单个值 - userConfig[key] = currentConfig.args[mapping.position ?? 0]; - } else if ( - mapping.type === "env" && - mapping.key && - currentConfig.env - ) { - // 对于 env 类型,从环境变量中获取值 - userConfig[key] = currentConfig.env[mapping.key]; - } - }); - setUserConfig(userConfig); - } - } else { - setUserConfig({}); + if (!editingServerId || !config) return; + const currentConfig = config.mcpServers[editingServerId]; + if (currentConfig) { + // 从当前配置中提取用户配置 + const preset = presetServers.find((s) => s.id === editingServerId); + if (preset?.configSchema) { + const userConfig: Record = {}; + Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => { + if (mapping.type === "spread") { + // For spread types, extract the array from args. + const startPos = mapping.position ?? 0; + userConfig[key] = currentConfig.args.slice(startPos); + } else if (mapping.type === "single") { + // For single types, get a single value + userConfig[key] = currentConfig.args[mapping.position ?? 0]; + } else if ( + mapping.type === "env" && + mapping.key && + currentConfig.env + ) { + // For env types, get values from environment variables + userConfig[key] = currentConfig.env[mapping.key]; + } + }); + setUserConfig(userConfig); } + } else { + setUserConfig({}); } }, [editingServerId, config, presetServers]); + if (!mcpEnabled) { + return null; + } + + // 检查服务器是否已添加 + const isServerAdded = (id: string) => { + return id in (config?.mcpServers ?? {}); + }; + // 保存服务器配置 const saveServerConfig = async () => { const preset = presetServers.find((s) => s.id === editingServerId); @@ -291,8 +312,8 @@ export function McpMarketPage() { } }; - // 修改恢复服务器函数 - const resumeServer = async (id: string) => { + // Restart server + const restartServer = async (id: string) => { try { updateLoadingState(id, "Starting server..."); @@ -320,7 +341,7 @@ export function McpMarketPage() { } }; - // 修改重启所有客户端函数 + // Restart all clients const handleRestartAll = async () => { try { updateLoadingState("all", "Restarting all servers..."); @@ -342,7 +363,7 @@ export function McpMarketPage() { } }; - // 渲染配置表单 + // Render configuration form const renderConfigForm = () => { const preset = presetServers.find((s) => s.id === editingServerId); if (!preset?.configSchema) return null; @@ -422,12 +443,10 @@ export function McpMarketPage() { ); }; - // 检查服务器状态 const checkServerStatus = (clientId: string) => { return clientStatuses[clientId] || { status: "undefined", errorMsg: null }; }; - // 修改状态显示逻辑 const getServerStatusDisplay = (clientId: string) => { const status = checkServerStatus(clientId); @@ -450,7 +469,7 @@ export function McpMarketPage() { return statusMap[status.status]; }; - // 获取操作状态的类型 + // Get the type of operation status const getOperationStatusType = (message: string) => { if (message.toLowerCase().includes("stopping")) return "stopping"; if (message.toLowerCase().includes("starting")) return "starting"; @@ -496,15 +515,15 @@ export function McpMarketPage() { // 定义状态优先级 const statusPriority: Record = { - error: 0, // 错误状态最高优先级 - active: 1, // 已启动次之 - starting: 2, // 正在启动 - stopping: 3, // 正在停止 - paused: 4, // 已暂停 - undefined: 5, // 未配置最低优先级 + error: 0, // Highest priority for error status + active: 1, // Second for active + starting: 2, // Starting + stopping: 3, // Stopping + paused: 4, // Paused + undefined: 5, // Lowest priority for undefined }; - // 获取实际状态(包括加载状态) + // Get actual status (including loading status) const getEffectiveStatus = (status: string, loading?: string) => { if (loading) { const operationType = getOperationStatusType(loading); @@ -524,7 +543,7 @@ export function McpMarketPage() { ); } - // 状态相同时按名称排序 + // Sort by name when statuses are the same return a.name.localeCompare(b.name); }) .map((server) => ( @@ -591,7 +610,7 @@ export function McpMarketPage() { } text="Start" - onClick={() => resumeServer(server.id)} + onClick={() => restartServer(server.id)} disabled={isLoading} /> {/* )} - {/*支持的Tools*/} {viewingServerId && (
(await import("./chat-list")).ChatList, { loading: () => null, @@ -129,6 +130,7 @@ export function useDragSideBar() { shouldNarrow, }; } + export function SideBarContainer(props: { children: React.ReactNode; onDragStart: (e: MouseEvent) => void; @@ -224,6 +226,17 @@ export function SideBar(props: { className?: string }) { const navigate = useNavigate(); const config = useAppConfig(); const chatStore = useChatStore(); + const [mcpEnabled, setMcpEnabled] = useState(false); + + useEffect(() => { + // 检查 MCP 是否启用 + const checkMcpStatus = async () => { + const enabled = await isMcpEnabled(); + setMcpEnabled(enabled); + console.log("[SideBar] MCP enabled:", enabled); + }; + checkMcpStatus(); + }, []); return ( - } - text={shouldNarrow ? undefined : Locale.Mcp.Name} - className={styles["sidebar-bar-button"]} - onClick={() => { - navigate(Path.McpMarket, { state: { fromHome: true } }); - }} - shadow - /> + {mcpEnabled && ( + } + text={shouldNarrow ? undefined : Locale.Mcp.Name} + className={styles["sidebar-bar-button"]} + onClick={() => { + navigate(Path.McpMarket, { state: { fromHome: true } }); + }} + shadow + /> + )} } text={shouldNarrow ? undefined : Locale.Discovery.Name} diff --git a/app/config/server.ts b/app/config/server.ts index 9d6b3c2b8da..ab7a775c2dc 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -81,6 +81,8 @@ declare global { // custom template for preprocessing user input DEFAULT_INPUT_TEMPLATE?: string; + + ENABLE_MCP?: string; // enable mcp functionality } } } @@ -129,7 +131,9 @@ export const getServerSideConfig = () => { if (customModels) customModels += ","; customModels += DEFAULT_MODELS.filter( (m) => - (m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o") || m.name.startsWith("o1")) && + (m.name.startsWith("gpt-4") || + m.name.startsWith("chatgpt-4o") || + m.name.startsWith("o1")) && !m.name.startsWith("gpt-4o-mini"), ) .map((m) => "-" + m.name) @@ -249,5 +253,6 @@ export const getServerSideConfig = () => { customModels, defaultModel, allowedWebDavEndpoints, + enableMcp: !!process.env.ENABLE_MCP, }; }; diff --git a/app/layout.tsx b/app/layout.tsx index 7d14cb88d70..47c058fb300 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,9 +5,8 @@ import "./styles/highlight.scss"; import { getClientConfig } from "./config/client"; import type { Metadata, Viewport } from "next"; import { SpeedInsights } from "@vercel/speed-insights/next"; -import { getServerSideConfig } from "./config/server"; import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google"; -const serverConfig = getServerSideConfig(); +import { getServerSideConfig } from "./config/server"; export const metadata: Metadata = { title: "NextChat", @@ -33,6 +32,8 @@ export default function RootLayout({ }: { children: React.ReactNode; }) { + const serverConfig = getServerSideConfig(); + return ( diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index 2248d1327c6..7d4b5b661dd 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -16,6 +16,7 @@ import { } from "./types"; import fs from "fs/promises"; import path from "path"; +import { getServerSideConfig } from "../config/server"; const logger = new MCPClientLogger("MCP Actions"); const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json"); @@ -117,6 +118,12 @@ async function initializeSingleClient( 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)) { @@ -352,3 +359,14 @@ export async function reinitializeClient(clientId: string) { } await initializeSingleClient(clientId, serverConfig); } + +// 检查 MCP 是否启用 +export async function isMcpEnabled() { + try { + const serverConfig = getServerSideConfig(); + return !!serverConfig.enableMcp; + } catch (error) { + logger.error(`Failed to check MCP status: ${error}`); + return false; + } +} diff --git a/app/page.tsx b/app/page.tsx index 48a70220190..c748d42c71a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,14 +1,10 @@ import { Analytics } from "@vercel/analytics/react"; import { Home } from "./components/home"; import { getServerSideConfig } from "./config/server"; -import { initializeMcpSystem } from "./mcp/actions"; const serverConfig = getServerSideConfig(); export default async function App() { - // 初始化 MCP 系统 - await initializeMcpSystem(); - return ( <> From bfeea4ed4996c103d5ee36a908d6726e82472300 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sun, 19 Jan 2025 01:02:01 +0800 Subject: [PATCH 28/29] fix: prevent MCP operations from blocking chat interface --- app/components/mcp-market.module.scss | 5 + app/components/mcp-market.tsx | 107 ++++++++------------- app/mcp/actions.ts | 129 ++++++++++++++------------ app/mcp/types.ts | 18 +++- 4 files changed, 132 insertions(+), 127 deletions(-) diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss index f5c8c0ccae3..283436c7f84 100644 --- a/app/components/mcp-market.module.scss +++ b/app/components/mcp-market.module.scss @@ -167,6 +167,11 @@ background-color: #6b7280; } + &.initializing { + background-color: #f59e0b; + animation: pulse 1.5s infinite; + } + .error-message { margin-left: 4px; font-size: 12px; diff --git a/app/components/mcp-market.tsx b/app/components/mcp-market.tsx index 98211ceddbf..235f63b1ca3 100644 --- a/app/components/mcp-market.tsx +++ b/app/components/mcp-market.tsx @@ -13,7 +13,7 @@ import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; import { addMcpServer, - getClientStatus, + getClientsStatus, getClientTools, getMcpConfigFromFile, isMcpEnabled, @@ -71,6 +71,23 @@ export function McpMarketPage() { checkMcpStatus(); }, [navigate]); + // 添加状态轮询 + useEffect(() => { + if (!mcpEnabled || !config) return; + + const updateStatuses = async () => { + const statuses = await getClientsStatus(); + setClientStatuses(statuses); + }; + + // 立即执行一次 + updateStatuses(); + // 每 1000ms 轮询一次 + const timer = setInterval(updateStatuses, 1000); + + return () => clearInterval(timer); + }, [mcpEnabled, config]); + // 加载预设服务器 useEffect(() => { const loadPresetServers = async () => { @@ -103,10 +120,7 @@ export function McpMarketPage() { setConfig(config); // 获取所有客户端的状态 - const statuses: Record = {}; - for (const clientId of Object.keys(config.mcpServers)) { - statuses[clientId] = await getClientStatus(clientId); - } + const statuses = await getClientsStatus(); setClientStatuses(statuses); } catch (error) { console.error("Failed to load initial state:", error); @@ -165,7 +179,6 @@ export function McpMarketPage() { const preset = presetServers.find((s) => s.id === editingServerId); if (!preset || !preset.configSchema || !editingServerId) return; - // 先关闭模态框 const savingServerId = editingServerId; setEditingServerId(undefined); @@ -200,31 +213,8 @@ export function McpMarketPage() { ...(Object.keys(env).length > 0 ? { env } : {}), }; - // 检查是否是新增还是编辑 - const isNewServer = !isServerAdded(savingServerId); - - // 如果是编辑现有服务器,保持原有状态 - if (!isNewServer) { - const currentConfig = await getMcpConfigFromFile(); - const currentStatus = currentConfig.mcpServers[savingServerId]?.status; - if (currentStatus) { - serverConfig.status = currentStatus; - } - } - - // 更新配置并初始化新服务器 const newConfig = await addMcpServer(savingServerId, serverConfig); setConfig(newConfig); - - // 只有新增的服务器才需要获取状态(因为会自动启动) - if (isNewServer) { - const status = await getClientStatus(savingServerId); - setClientStatuses((prev) => ({ - ...prev, - [savingServerId]: status, - })); - } - showToast("Server configuration updated successfully"); } catch (error) { showToast( @@ -277,11 +267,8 @@ export function McpMarketPage() { setConfig(newConfig); // 更新状态 - const status = await getClientStatus(preset.id); - setClientStatuses((prev) => ({ - ...prev, - [preset.id]: status, - })); + const statuses = await getClientsStatus(); + setClientStatuses(statuses); } finally { updateLoadingState(preset.id, null); } @@ -298,11 +285,6 @@ export function McpMarketPage() { updateLoadingState(id, "Stopping server..."); const newConfig = await pauseMcpServer(id); setConfig(newConfig); - - setClientStatuses((prev) => ({ - ...prev, - [id]: { status: "paused", errorMsg: null }, - })); showToast("Server stopped successfully"); } catch (error) { showToast("Failed to stop server"); @@ -316,19 +298,7 @@ export function McpMarketPage() { const restartServer = async (id: string) => { try { updateLoadingState(id, "Starting server..."); - - const success = await resumeMcpServer(id); - const status = await getClientStatus(id); - setClientStatuses((prev) => ({ - ...prev, - [id]: status, - })); - - if (success) { - showToast("Server started successfully"); - } else { - throw new Error("Failed to start server"); - } + await resumeMcpServer(id); } catch (error) { showToast( error instanceof Error @@ -347,14 +317,7 @@ export function McpMarketPage() { updateLoadingState("all", "Restarting all servers..."); const newConfig = await restartAllClients(); setConfig(newConfig); - - const statuses: Record = {}; - for (const clientId of Object.keys(newConfig.mcpServers)) { - statuses[clientId] = await getClientStatus(clientId); - } - setClientStatuses(statuses); - - showToast("Successfully restarted all clients"); + showToast("Restarting all clients"); } catch (error) { showToast("Failed to restart clients"); console.error(error); @@ -452,6 +415,12 @@ export function McpMarketPage() { const statusMap = { undefined: null, // 未配置/未找到不显示 + // 添加初始化状态 + initializing: ( + + Initializing + + ), paused: ( Stopped @@ -517,10 +486,11 @@ export function McpMarketPage() { const statusPriority: Record = { error: 0, // Highest priority for error status active: 1, // Second for active - starting: 2, // Starting - stopping: 3, // Stopping - paused: 4, // Paused - undefined: 5, // Lowest priority for undefined + initializing: 2, // Initializing + starting: 3, // Starting + stopping: 4, // Stopping + paused: 5, // Paused + undefined: 6, // Lowest priority for undefined }; // Get actual status (including loading status) @@ -529,6 +499,11 @@ export function McpMarketPage() { const operationType = getOperationStatusType(loading); return operationType === "default" ? status : operationType; } + + if (status === "initializing" && !loading) { + return "active"; + } + return status; }; @@ -538,8 +513,8 @@ export function McpMarketPage() { // 首先按状态排序 if (aEffectiveStatus !== bEffectiveStatus) { return ( - (statusPriority[aEffectiveStatus] ?? 5) - - (statusPriority[bEffectiveStatus] ?? 5) + (statusPriority[aEffectiveStatus] ?? 6) - + (statusPriority[bEffectiveStatus] ?? 6) ); } diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index 7d4b5b661dd..b4611d93409 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -24,40 +24,54 @@ const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json"); const clientsMap = new Map(); // 获取客户端状态 -export async function getClientStatus( - clientId: string, -): Promise { - const status = clientsMap.get(clientId); +export async function getClientsStatus(): Promise< + Record +> { const config = await getMcpConfigFromFile(); - const serverConfig = config.mcpServers[clientId]; + const result: Record = {}; - // 如果配置中不存在该服务器 - if (!serverConfig) { - return { status: "undefined", errorMsg: null }; - } + for (const clientId of Object.keys(config.mcpServers)) { + const status = clientsMap.get(clientId); + const serverConfig = config.mcpServers[clientId]; - // 如果服务器配置为暂停状态 - if (serverConfig.status === "paused") { - return { status: "paused", errorMsg: null }; - } + if (!serverConfig) { + result[clientId] = { status: "undefined", errorMsg: null }; + continue; + } - // 如果 clientsMap 中没有记录 - if (!status) { - return { status: "undefined", errorMsg: null }; - } + if (serverConfig.status === "paused") { + result[clientId] = { status: "paused", errorMsg: null }; + continue; + } - // 如果有错误 - if (status.errorMsg) { - return { status: "error", errorMsg: status.errorMsg }; - } + if (!status) { + result[clientId] = { status: "undefined", errorMsg: null }; + continue; + } - // 如果客户端正常运行 - if (status.client) { - return { status: "active", errorMsg: null }; + 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 { status: "error", errorMsg: "Client not found" }; + return result; } // 获取客户端工具 @@ -96,22 +110,32 @@ async function initializeSingleClient( } logger.info(`Initializing client [${clientId}]...`); - try { - const client = await createClient(clientId, serverConfig); - 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), + + // 先设置初始化状态 + 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}`); }); - logger.error(`Failed to initialize client [${clientId}]: ${error}`); - } } // 初始化系统 @@ -184,7 +208,7 @@ export async function pauseMcpServer(clientId: string) { ...currentConfig.mcpServers, [clientId]: { ...serverConfig, - status: "paused" as const, + status: "paused", }, }, }; @@ -205,7 +229,7 @@ export async function pauseMcpServer(clientId: string) { } // 恢复服务器 -export async function resumeMcpServer(clientId: string): Promise { +export async function resumeMcpServer(clientId: string): Promise { try { const currentConfig = await getMcpConfigFromFile(); const serverConfig = currentConfig.mcpServers[clientId]; @@ -233,10 +257,6 @@ export async function resumeMcpServer(clientId: string): Promise { }, }; await updateMcpConfig(newConfig); - - // 再次确认状态 - const status = await getClientStatus(clientId); - return status.status === "active"; } catch (error) { const currentConfig = await getMcpConfigFromFile(); const serverConfig = currentConfig.mcpServers[clientId]; @@ -254,7 +274,7 @@ export async function resumeMcpServer(clientId: string): Promise { errorMsg: error instanceof Error ? error.message : String(error), }); logger.error(`Failed to initialize client [${clientId}]: ${error}`); - return false; + throw error; } } catch (error) { logger.error(`Failed to resume server [${clientId}]: ${error}`); @@ -297,6 +317,7 @@ export async function restartAllClients() { await removeClient(client.client); } } + // 清空状态 clientsMap.clear(); @@ -350,21 +371,11 @@ async function updateMcpConfig(config: McpConfigData): Promise { } } -// 重新初始化单个客户端 -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); -} - // 检查 MCP 是否启用 export async function isMcpEnabled() { try { const serverConfig = getServerSideConfig(); - return !!serverConfig.enableMcp; + return serverConfig.enableMcp; } catch (error) { logger.error(`Failed to check MCP status: ${error}`); return false; diff --git a/app/mcp/types.ts b/app/mcp/types.ts index 85e94f3b8a6..45d1d979a98 100644 --- a/app/mcp/types.ts +++ b/app/mcp/types.ts @@ -73,7 +73,16 @@ export interface ListToolsResponse { }; } -export type McpClientData = McpActiveClient | McpErrorClient; +export type McpClientData = + | McpActiveClient + | McpErrorClient + | McpInitializingClient; + +interface McpInitializingClient { + client: null; + tools: null; + errorMsg: null; +} interface McpActiveClient { client: Client; @@ -88,7 +97,12 @@ interface McpErrorClient { } // 服务器状态类型 -export type ServerStatus = "undefined" | "active" | "paused" | "error"; +export type ServerStatus = + | "undefined" + | "active" + | "paused" + | "error" + | "initializing"; export interface ServerStatusResponse { status: ServerStatus; From 611e97e641d9d8b6c80e36da29fa21a2705f972d Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sun, 19 Jan 2025 23:20:58 +0800 Subject: [PATCH 29/29] docs: update README.md --- README_CN.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README_CN.md b/README_CN.md index 31b596f0bbd..d5b3c12a294 100644 --- a/README_CN.md +++ b/README_CN.md @@ -254,6 +254,10 @@ Stability API密钥 自定义的Stability API请求地址 +### `ENABLE_MCP` (optional) + +启用MCP(Model Context Protocol)功能 + ## 开发 @@ -307,6 +311,16 @@ docker run -d -p 3000:3000 \ yidadaa/chatgpt-next-web ``` +如需启用 MCP 功能,可以使用: + +```shell +docker run -d -p 3000:3000 \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=页面访问密码 \ + -e ENABLE_MCP=true \ + yidadaa/chatgpt-next-web +``` + 如果你的本地代理需要账号密码,可以使用: ```shell