From e1c7c54dfaf82c37450d0ed3a124f8598bc0249b Mon Sep 17 00:00:00 2001 From: river Date: Mon, 23 Dec 2024 22:32:36 +0800 Subject: [PATCH 01/32] 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 8173b9c4d..31b596f0b 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/32] 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 000000000..3d6ca4a68 --- /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 000000000..d71314f3a --- /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 000000000..d924ba664 --- /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 000000000..a39304afe --- /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 000000000..044d04052 --- /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 63d7394ec..27d1f8620 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 2bb6bc4f4..802419139 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 e081567a4..a17f8ffa9 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 c73eef3e8..6d24b42f1 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 dffc35e9c..138f3c851 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/32] 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 089752554..8109e6bec 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 3d6ca4a68..af8683440 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; } - return fsClient; + + 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}`); + } + } + + 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(",")}`); } -export async function executeMcpAction(request: any) { - "use server"; - +// 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 d71314f3a..7eb55fb82 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 d924ba664..83fc8784c 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 a39304afe..25129c592 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 000000000..6ad18236b --- /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 044d04052..000000000 --- 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 b3f169a9b..d4ba2a276 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 27d1f8620..3444bb436 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 a17f8ffa9..0efe27b39 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 138f3c851..5b9741b2b 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/32] 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 000000000..5b6dcbf02 --- /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 3444bb436..d30fa1fea 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/32] 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 af8683440..5fe611b3a 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 7eb55fb82..0600f00be 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 000000000..763121bad --- /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 d30fa1fea..e0ee95621 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); + // MCP Response no need to fill template + let mContent: string | MultimodalContent[] = isMcpResponse + ? content + : fillTemplateWith(content, modelConfig); - let mContent: string | MultimodalContent[] = userContent; - - 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/32] feat: carry mcp primitives content as a system prompt --- app/components/chat.tsx | 503 +++++++++++++++++++++------------------- app/constant.ts | 106 +++++++++ app/mcp/actions.ts | 36 ++- app/mcp/client.ts | 4 +- app/mcp/example.ts | 26 +-- app/store/chat.ts | 48 +++- 6 files changed, 448 insertions(+), 275 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 51fe74fe7..75120041c 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; + {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; + 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; + 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], + }, + }); } - }, - ); - }} - > -
- {isUser ? ( - - ) : ( - <> - {["system"].includes(message.role) ? ( - - ) : ( - - )} - + 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 && ( +
+ {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={() => - openaiSpeech( + onDelete(message.id ?? i) + } + /> + + } + onClick={() => onPinMessage(message)} + /> + } + onClick={() => + 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) => ( -
- {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 && ( - + {/*@ts-ignore*/} + {message?.tools?.length > 0 && ( +
+ {message?.tools?.map((tool) => ( +
+ {tool.isError === false ? ( + + ) : tool.isError === true ? ( + + ) : ( + + )} + {tool?.function?.name} +
+ ))} +
)} - {getMessageImages(message).length > 1 && ( -
+ - {getMessageImages(message).map((image, index) => { - return ( - - ); - })} + // onContextMenu={(e) => 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 0600f00be..6650f9e2b 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 83fc8784c..f3b91fb8c 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); -async function main() { - logger.info("Connecting to server..."); +const TEST_SERVER = "everything"; - const client = await createClient(conf.mcpServers.everything, "everything"); +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); - 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 e0ee95621..80c706ffd 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/32] 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 544e2a246..9d15b5fa1 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 80c706ffd..93bbde99d 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/32] 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 6ad18236b..3a8b3afaa 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 93bbde99d..4a70c9296 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/32] 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 5da490378..32c5b4ac6 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 000000000..5e4b6e9b0 --- /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 000000000..5f0723e39 --- /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 a5e33b15e..84b0973bd 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 9d15b5fa1..3c0ff6213 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 000000000..aaf0bbc74 --- /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 47be019a8..bd8b53060 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 ad07bb428..f9a6afc86 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 3a8b3afaa..ee092d7f0 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 000000000..0daec9aeb --- /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 763121bad..a97c94e05 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 5b6dcbf02..b74509881 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 802419139..0e1105d56 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 5b9741b2b..a99ff0804 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/32] 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 75120041c..bbc4444f6 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/32] 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 5f0723e39..e754c413c 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 f9a6afc86..bf38dcc63 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 ee092d7f0..e778108de 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/32] 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 e754c413c..926e64b29 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 e778108de..ee092d7f0 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/32] 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 ee092d7f0..da39e4ffa 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/32] feat: ignore mcp_config.json --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2ff556f64..b1c2bfefa 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/32] feat: Optimize MCP configuration logic --- app/components/chat.tsx | 24 ++ app/components/home.tsx | 10 + app/components/mcp-market.module.scss | 194 ++++----- app/components/mcp-market.tsx | 589 ++++++++++++++------------ app/constant.ts | 11 +- app/icons/tool.svg | 5 + app/mcp/actions.ts | 373 ++++++++-------- 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, 766 insertions(+), 653 deletions(-) create mode 100644 app/icons/tool.svg diff --git a/app/components/chat.tsx b/app/components/chat.tsx index bbc4444f6..c8d6886e5 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 32c5b4ac6..8a03c50b6 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 5e4b6e9b0..93c6b67de 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 { + &.error { + background-color: #ef4444; + } + + .error-message { + margin-left: 4px; 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; + .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); - } - } - - &.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; + svg { + width: 14px; + height: 14px; } } - } - @media screen and (max-width: 600px) { - flex-direction: column; - gap: 10px; + .tags-container { + display: flex; + gap: 4px; + flex-wrap: wrap; + margin-bottom: 8px; + } + + .tag { + background: var(--gray); + color: var(--black); + padding: 2px 6px; + border-radius: 4px; + font-size: 10px; + opacity: 0.8; + } + + .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; + + :global(.icon-button) { + transition: all 0.3s ease; + border: 1px solid transparent; + + &:hover { + transform: translateY(-1px); + filter: brightness(1.1); + } + } } } } @@ -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 926e64b29..d93754549 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 [editingServerId, setEditingServerId] = useState(); - const [viewingServerId, setViewingServerId] = useState(); - const [primitives, setPrimitives] = useState([]); const [userConfig, setUserConfig] = useState>({}); + const [editingServerId, setEditingServerId] = useState(); + const [tools, setTools] = useState(null); + const [viewingServerId, setViewingServerId] = 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 3c0ff6213..9cdf197bf 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 000000000..f7543e201 --- /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 bf38dcc63..6b5ea6df3 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"); - -// Use Map to store all clients -const clientsMap = new Map< - string, - { client: Client | null; primitives: Primitive[]; errorMsg: string | null } ->(); - -// Whether initialized -let initialized = false; - -// Store failed clients -let errorClients: string[] = []; - const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json"); -// 获取 MCP 配置 -export async function getMcpConfig(): Promise { - try { - const configStr = await fs.readFile(CONFIG_PATH, "utf-8"); - return JSON.parse(configStr); - } catch (error) { - console.error("Failed to read MCP config:", error); - return { mcpServers: {} }; - } +const clientsMap = new Map(); + +// 获取客户端状态 +export async function getClientStatus(clientId: string) { + const status = clientsMap.get(clientId); + if (!status) return { status: "undefined" as const, errorMsg: null }; + + return { + status: status.errorMsg ? ("error" as const) : ("active" as const), + errorMsg: status.errorMsg, + }; } -// 更新 MCP 配置 -export async function updateMcpConfig(config: McpConfig): Promise { - try { - await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2)); - } catch (error) { - console.error("Failed to write MCP config:", error); - throw error; - } +// 获取客户端工具 +export async function getClientTools(clientId: string) { + return clientsMap.get(clientId)?.tools ?? null; } -// 重新初始化所有客户端 -export async function reinitializeMcpClients() { - logger.info("Reinitializing MCP clients..."); - // 遍历所有客户端,关闭 - try { - for (const [clientId, clientData] of clientsMap.entries()) { - clientData.client?.close(); +// 获取可用客户端数量 +export async function getAvailableClientsCount() { + let count = 0; + clientsMap.forEach((map) => { + if (!map.errorMsg) { + count += map?.tools?.tools?.length ?? 0; } - } catch (error) { - logger.error(`Failed to close clients: ${error}`); - } - // 清空状态 - clientsMap.clear(); - errorClients = []; - initialized = false; - // 重新初始化 - return initializeMcpClients(); + }); + return count; } -// Initialize all configured clients -export async function initializeMcpClients() { - // If already initialized, return - if (initialized) { - return { errorClients }; +// 获取所有客户端工具 +export async function getAllTools() { + const result = []; + for (const [clientId, status] of clientsMap.entries()) { + result.push({ + clientId, + tools: status.tools, + }); } - - logger.info("Starting to initialize MCP clients..."); - errorClients = []; - - const config = await getMcpConfig(); - // Initialize all clients, key is clientId, value is client config - for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) { - try { - logger.info(`Initializing MCP client: ${clientId}`); - const client = await createClient(serverConfig as ServerConfig, clientId); - const primitives = await listPrimitives(client); - clientsMap.set(clientId, { client, primitives, errorMsg: null }); - logger.success( - `Client [${clientId}] initialized, ${primitives.length} primitives supported`, - ); - } catch (error) { - errorClients.push(clientId); - clientsMap.set(clientId, { - client: null, - primitives: [], - errorMsg: error instanceof Error ? error.message : String(error), - }); - logger.error(`Failed to initialize client ${clientId}: ${error}`); - } - } - - initialized = true; - - if (errorClients.length > 0) { - logger.warn(`Failed to initialize clients: ${errorClients.join(", ")}`); - } else { - logger.success("All MCP clients initialized"); - } - - const availableClients = await getAvailableClients(); - logger.info(`Available clients: ${availableClients.join(",")}`); - - return { errorClients }; + return result; } -// Execute MCP request -export async function executeMcpAction( +// 初始化单个客户端 +async function initializeSingleClient( clientId: string, - request: McpRequestMessage, + serverConfig: ServerConfig, ) { + logger.info(`Initializing client [${clientId}]...`); try { - // Find the corresponding client - const client = clientsMap.get(clientId)?.client; - if (!client) { - logger.error(`Client ${clientId} not found`); - return; - } - - logger.info(`Executing MCP request for ${clientId}`); - - // Execute request and return result - return await executeRequest(client, request); + const client = await createClient(clientId, serverConfig); + const tools = await listTools(client); + clientsMap.set(clientId, { client, tools, errorMsg: null }); + logger.success(`Client [${clientId}] initialized successfully`); } catch (error) { - logger.error(`MCP execution error: ${error}`); + clientsMap.set(clientId, { + client: null, + tools: null, + errorMsg: error instanceof Error ? error.message : String(error), + }); + logger.error(`Failed to initialize client [${clientId}]: ${error}`); + } +} + +// 初始化系统 +export async function initializeMcpSystem() { + logger.info("MCP Actions starting..."); + try { + const config = await getMcpConfigFromFile(); + // 初始化所有客户端 + for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) { + await initializeSingleClient(clientId, serverConfig); + } + return config; + } catch (error) { + logger.error(`Failed to initialize MCP system: ${error}`); throw error; } } -// Get all available client IDs -export async function getAvailableClients() { - return Array.from(clientsMap.entries()) - .filter(([_, data]) => data.errorMsg === null) - .map(([clientId]) => clientId); -} - -// Get all primitives from all clients -export async function getAllPrimitives(): Promise< - { - clientId: string; - primitives: Primitive[]; - }[] -> { - return Array.from(clientsMap.entries()).map(([clientId, { primitives }]) => ({ - clientId, - primitives, - })); -} - -// 获取客户端的 Primitives -export async function getClientPrimitives(clientId: string) { +// 添加服务器 +export async function addMcpServer(clientId: string, config: ServerConfig) { try { - const clientData = clientsMap.get(clientId); - if (!clientData) { - console.warn(`Client ${clientId} not found in map`); - return null; - } - if (clientData.errorMsg) { - console.warn(`Client ${clientId} has error: ${clientData.errorMsg}`); - return null; - } - return clientData.primitives; + const currentConfig = await getMcpConfigFromFile(); + const newConfig = { + ...currentConfig, + mcpServers: { + ...currentConfig.mcpServers, + [clientId]: config, + }, + }; + await updateMcpConfig(newConfig); + // 只初始化新添加的服务器 + await initializeSingleClient(clientId, config); + return newConfig; } catch (error) { - console.error(`Failed to get primitives for client ${clientId}:`, error); - return null; + logger.error(`Failed to add server [${clientId}]: ${error}`); + throw error; + } +} + +// 移除服务器 +export async function removeMcpServer(clientId: string) { + try { + const currentConfig = await getMcpConfigFromFile(); + const { [clientId]: _, ...rest } = currentConfig.mcpServers; + const newConfig = { + ...currentConfig, + mcpServers: rest, + }; + await updateMcpConfig(newConfig); + + // 关闭并移除客户端 + const client = clientsMap.get(clientId); + if (client?.client) { + await removeClient(client.client); + } + clientsMap.delete(clientId); + + return newConfig; + } catch (error) { + logger.error(`Failed to remove server [${clientId}]: ${error}`); + throw error; } } // 重启所有客户端 export async function restartAllClients() { - logger.info("Restarting all MCP clients..."); - - // 清空状态 - clientsMap.clear(); - errorClients = []; - initialized = false; - - // 重新初始化 - await initializeMcpClients(); - - return { - success: errorClients.length === 0, - errorClients, - }; -} - -// 获取所有客户端状态 -export async function getAllClientStatus(): Promise< - Record -> { - 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; -} - -// 获取客户端状态,不重新初始化 -export async function refreshClientStatus() { - logger.info("Refreshing client status..."); - - // 如果还没初始化过,则初始化 - if (!initialized) { - return initializeMcpClients(); - } - - // 否则只更新错误状态 - errorClients = []; - for (const [clientId, clientData] of clientsMap.entries()) { - if (clientData.errorMsg !== null) { - errorClients.push(clientId); + logger.info("Restarting all clients..."); + try { + // 关闭所有客户端 + for (const client of clientsMap.values()) { + if (client.client) { + await removeClient(client.client); + } } - } + // 清空状态 + clientsMap.clear(); - return { errorClients }; + // 重新初始化 + const config = await getMcpConfigFromFile(); + for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) { + await initializeSingleClient(clientId, serverConfig); + } + return config; + } catch (error) { + logger.error(`Failed to restart clients: ${error}`); + throw error; + } +} + +// 执行 MCP 请求 +export async function executeMcpAction( + clientId: string, + request: McpRequestMessage, +) { + try { + const client = clientsMap.get(clientId); + if (!client?.client) { + throw new Error(`Client ${clientId} not found`); + } + logger.info(`Executing request for [${clientId}]`); + return await executeRequest(client.client, request); + } catch (error) { + logger.error(`Failed to execute request for [${clientId}]: ${error}`); + throw error; + } +} + +// 获取 MCP 配置文件 +export async function getMcpConfigFromFile(): Promise { + try { + const configStr = await fs.readFile(CONFIG_PATH, "utf-8"); + return JSON.parse(configStr); + } catch (error) { + logger.error(`Failed to load MCP config, using default config: ${error}`); + return DEFAULT_MCP_CONFIG; + } +} + +// 更新 MCP 配置文件 +async function updateMcpConfig(config: McpConfigData): Promise { + try { + await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2)); + } catch (error) { + throw error; + } +} + +// 重新初始化单个客户端 +export async function reinitializeClient(clientId: string) { + const config = await getMcpConfigFromFile(); + const serverConfig = config.mcpServers[clientId]; + if (!serverConfig) { + throw new Error(`Server config not found for client ${clientId}`); + } + await initializeSingleClient(clientId, serverConfig); } diff --git a/app/mcp/client.ts b/app/mcp/client.ts index 6650f9e2b..b7b511a92 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 f3b91fb8c..986196d63 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 da39e4ffa..8a235acc9 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 0daec9aeb..b44b841d2 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 a97c94e05..da6731d28 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 d4ba2a276..48a702201 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 4a70c9296..6c6c70a1c 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 primitivesString = ""; - primitives.forEach((i) => { - primitivesString += MCP_PRIMITIVES_TEMPLATE.replace( + let toolsStr = ""; + + tools.forEach((i) => { + // error client has no tools + if (!i.tools) return; + + 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/32] =?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 8a235acc9..000000000 --- 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/32] 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 d93754549..fc088c03b 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 6b5ea6df3..c6b9fd75f 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/32] 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 f7543e201..add538457 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/32] 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 b7b511a92..5c2f071e3 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/32] 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 93c6b67de..a3025c03e 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 fc088c03b..0e46e7766 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 4e81ef067..08a6572d6 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 000000000..4a2515c6f --- /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 c6b9fd75f..ba1525be7 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]; - return { - status: status.errorMsg ? ("error" as const) : ("active" as const), - errorMsg: status.errorMsg, - }; + // 如果配置中不存在该服务器 + 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: "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 b44b841d2..84fe234bd 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 da6731d28..85e94f3b8 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/32] 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 a3025c03e..46f3c3368 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 0e46e7766..bbf0d4d45 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/32] 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 bbf0d4d45..0bd4a7dd6 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/32] 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 0bd4a7dd6..9aff190b8 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 986196d63..000000000 --- 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 84fe234bd..000000000 --- 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/32] chore: enhance mcp prompt --- app/constant.ts | 85 ++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/app/constant.ts b/app/constant.ts index 9cdf197bf..ed244068e 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 - { - "directories": ["/path/to/desktop"] - } - \`\`\`" - Assistant: "I can see that I have access to your desktop directory. Let me list its contents for you. + \`\`\`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 + { + "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.!!!!!!!!!!! + + 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/32] 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 46f3c3368..f5c8c0cca 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 9aff190b8..a7cea879d 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 ba1525be7..2248d1327 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/32] 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 fddb6f091..6ceb425dd 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/32] 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 82f44216a..c0cd80c65 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 ae9a17cdd..ff009b178 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 c8d6886e5..435a13b76 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 8a03c50b6..98f759a48 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 a7cea879d..98211cedd 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(); - }, []); + loadPresetServers(); + }, [mcpEnabled]); - // 检查服务器是否已添加 - const isServerAdded = (id: string) => { - return id in (config?.mcpServers ?? {}); - }; - - // 从服务器获取初始状态 + // 加载初始状态 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 9d6b3c2b8..ab7a775c2 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 7d14cb88d..47c058fb3 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 2248d1327..7d4b5b661 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 48a702201..c748d42c7 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/32] 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 | 137 ++++++++++++++------------ app/mcp/types.ts | 18 +++- 4 files changed, 136 insertions(+), 131 deletions(-) diff --git a/app/components/mcp-market.module.scss b/app/components/mcp-market.module.scss index f5c8c0cca..283436c7f 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 98211cedd..235f63b1c 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 7d4b5b661..b4611d934 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) { + result[clientId] = { status: "undefined", errorMsg: null }; + continue; + } + + if (serverConfig.status === "paused") { + result[clientId] = { status: "paused", errorMsg: null }; + continue; + } + + if (!status) { + result[clientId] = { status: "undefined", errorMsg: null }; + continue; + } + + 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" }; } - // 如果服务器配置为暂停状态 - 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: "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 85e94f3b8..45d1d979a 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/32] docs: update README.md --- README_CN.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README_CN.md b/README_CN.md index 31b596f0b..d5b3c12a2 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 From 8111acff34189ab980baca279c4fa811f63aac8b Mon Sep 17 00:00:00 2001 From: RiverRay Date: Mon, 20 Jan 2025 00:17:47 +0800 Subject: [PATCH 30/32] Update README.md --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 6310b4f5a..33a847397 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ +

NextChat (ChatGPT Next Web)

English / [简体中文](./README_CN.md) @@ -39,6 +40,12 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with Claude, GPT
+## 🫣 NextChat Support MCP ! +> Before build, please set env ENABLE_MCP=true + + + + ## Enterprise Edition Meeting Your Company's Privatization and Customization Deployment Requirements: @@ -333,6 +340,12 @@ Stability API key. Customize Stability API url. + +### `ENABLE_MCP` (optional) + +Enable MCP(Model Context Protocol)Feature + + ## Requirements NodeJS >= 18, Docker >= 20 @@ -391,6 +404,16 @@ If your proxy needs password, use: -e PROXY_URL="http://127.0.0.1:7890 user pass" ``` +If enable MCP, use: + +``` +docker run -d -p 3000:3000 \ + -e OPENAI_API_KEY=sk-xxxx \ + -e CODE=your-password \ + -e ENABLE_MCP=true \ + yidadaa/chatgpt-next-web +``` + ### Shell ```shell From f22cfd7b33a81c8f245001ccd772c94a6162a54b Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Mon, 20 Jan 2025 10:10:52 +0800 Subject: [PATCH 31/32] Update chat.tsx --- app/components/chat.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 323cc8b1c..6691403e6 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -72,6 +72,9 @@ import { safeLocalStorage, getModelSizes, supportsCustomSize, + useMobileScreen, + selectOrCopy, + showPlugins, } from "../utils"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; From 81bd83eb444ed5d0ebd6f7169de5944549bda32d Mon Sep 17 00:00:00 2001 From: RiverRay Date: Wed, 22 Jan 2025 13:08:33 +0800 Subject: [PATCH 32/32] Update README_CN.md --- README_CN.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README_CN.md b/README_CN.md index 2795f89c4..9348176e5 100644 --- a/README_CN.md +++ b/README_CN.md @@ -27,7 +27,8 @@ 企业版咨询: **business@nextchat.dev** - + + ## 开始使用