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

NextChat

-一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。 +一键免费部署你的私人 ChatGPT 网页应用,支持 Claude, GPT4 & Gemini Pro 模型。 [NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) From c3108ad333419ecb0d16a031d4f4603f0f781832 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sat, 28 Dec 2024 14:31:43 +0800 Subject: [PATCH 2/5] feat: simple MCP example --- app/mcp/actions.ts | 33 ++++++++++++++++ app/mcp/client.ts | 87 ++++++++++++++++++++++++++++++++++++++++ app/mcp/example.ts | 92 +++++++++++++++++++++++++++++++++++++++++++ app/mcp/logger.ts | 60 ++++++++++++++++++++++++++++ app/mcp/mcp_config.ts | 40 +++++++++++++++++++ app/store/chat.ts | 19 ++++++++- next.config.mjs | 9 +++-- package.json | 6 ++- tsconfig.json | 4 +- yarn.lock | 72 ++++++++++++++++++++++++++++++++- 10 files changed, 413 insertions(+), 9 deletions(-) create mode 100644 app/mcp/actions.ts create mode 100644 app/mcp/client.ts create mode 100644 app/mcp/example.ts create mode 100644 app/mcp/logger.ts create mode 100644 app/mcp/mcp_config.ts diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts new file mode 100644 index 00000000000..3d6ca4a68b8 --- /dev/null +++ b/app/mcp/actions.ts @@ -0,0 +1,33 @@ +"use server"; + +import { createClient, executeRequest } from "./client"; +import { MCPClientLogger } from "./logger"; +import { MCP_CONF } from "@/app/mcp/mcp_config"; + +const logger = new MCPClientLogger("MCP Server"); + +let fsClient: any = null; + +async function initFileSystemClient() { + if (!fsClient) { + fsClient = await createClient(MCP_CONF.filesystem, "fs"); + logger.success("FileSystem client initialized"); + } + return fsClient; +} + +export async function executeMcpAction(request: any) { + "use server"; + + try { + if (!fsClient) { + await initFileSystemClient(); + } + + logger.info("Executing MCP request for fs"); + return await executeRequest(fsClient, request); + } catch (error) { + logger.error(`MCP execution error: ${error}`); + throw error; + } +} diff --git a/app/mcp/client.ts b/app/mcp/client.ts new file mode 100644 index 00000000000..d71314f3ac9 --- /dev/null +++ b/app/mcp/client.ts @@ -0,0 +1,87 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { MCPClientLogger } from "./logger"; +import { z } from "zod"; + +export interface ServerConfig { + command: string; + args?: string[]; + env?: Record; +} + +const logger = new MCPClientLogger(); + +export async function createClient( + serverConfig: ServerConfig, + name: string, +): Promise { + logger.info(`Creating client for server ${name}`); + + const transport = new StdioClientTransport({ + command: serverConfig.command, + args: serverConfig.args, + env: serverConfig.env, + }); + const client = new Client( + { + name: `nextchat-mcp-client-${name}`, + version: "1.0.0", + }, + { + capabilities: { + roots: { + // listChanged indicates whether the client will emit notifications when the list of roots changes. + // listChanged 指示客户端在根列表更改时是否发出通知。 + listChanged: true, + }, + }, + }, + ); + await client.connect(transport); + return client; +} + +interface Primitive { + type: "resource" | "tool" | "prompt"; + value: any; +} + +/** List all resources, tools, and prompts */ +export async function listPrimitives(client: Client) { + const capabilities = client.getServerCapabilities(); + const primitives: Primitive[] = []; + const promises = []; + if (capabilities?.resources) { + promises.push( + client.listResources().then(({ resources }) => { + resources.forEach((item) => + primitives.push({ type: "resource", value: item }), + ); + }), + ); + } + if (capabilities?.tools) { + promises.push( + client.listTools().then(({ tools }) => { + tools.forEach((item) => primitives.push({ type: "tool", value: item })); + }), + ); + } + if (capabilities?.prompts) { + promises.push( + client.listPrompts().then(({ prompts }) => { + prompts.forEach((item) => + primitives.push({ type: "prompt", value: item }), + ); + }), + ); + } + await Promise.all(promises); + return primitives; +} + +export async function executeRequest(client: Client, request: any) { + const r = client.request(request, z.any()); + console.log(r); + return r; +} diff --git a/app/mcp/example.ts b/app/mcp/example.ts new file mode 100644 index 00000000000..d924ba66470 --- /dev/null +++ b/app/mcp/example.ts @@ -0,0 +1,92 @@ +import { createClient, listPrimitives } from "@/app/mcp/client"; +import { MCPClientLogger } from "@/app/mcp/logger"; +import { z } from "zod"; +import { MCP_CONF } from "@/app/mcp/mcp_config"; + +const logger = new MCPClientLogger("MCP FS Example", true); + +const ListAllowedDirectoriesResultSchema = z.object({ + content: z.array( + z.object({ + type: z.string(), + text: z.string(), + }), + ), +}); + +const ReadFileResultSchema = z.object({ + content: z.array( + z.object({ + type: z.string(), + text: z.string(), + }), + ), +}); + +async function main() { + logger.info("Connecting to server..."); + + const client = await createClient(MCP_CONF.filesystem, "fs"); + const primitives = await listPrimitives(client); + + logger.success(`Connected to server fs`); + + logger.info( + `server capabilities: ${Object.keys( + client.getServerCapabilities() ?? [], + ).join(", ")}`, + ); + + logger.debug("Server supports the following primitives:"); + + primitives.forEach((primitive) => { + logger.debug("\n" + JSON.stringify(primitive, null, 2)); + }); + + const listAllowedDirectories = async () => { + const result = await client.request( + { + method: "tools/call", + params: { + name: "list_allowed_directories", + arguments: {}, + }, + }, + ListAllowedDirectoriesResultSchema, + ); + logger.success(`Allowed directories: ${result.content[0].text}`); + return result; + }; + + const readFile = async (path: string) => { + const result = await client.request( + { + method: "tools/call", + params: { + name: "read_file", + arguments: { + path: path, + }, + }, + }, + ReadFileResultSchema, + ); + logger.success(`File contents for ${path}:\n${result.content[0].text}`); + return result; + }; + + try { + logger.info("Example 1: List allowed directories\n"); + await listAllowedDirectories(); + + logger.info("\nExample 2: Read a file\n"); + await readFile("/users/kadxy/desktop/test.txt"); + } catch (error) { + logger.error(`Error executing examples: ${error}`); + } +} + +main().catch((error) => { + logger.error(error); + process.exit(1); +}); diff --git a/app/mcp/logger.ts b/app/mcp/logger.ts new file mode 100644 index 00000000000..a39304afe91 --- /dev/null +++ b/app/mcp/logger.ts @@ -0,0 +1,60 @@ +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + dim: "\x1b[2m", + green: "\x1b[32m", + yellow: "\x1b[33m", + red: "\x1b[31m", + blue: "\x1b[34m", +}; + +export class MCPClientLogger { + private readonly prefix: string; + private readonly debugMode: boolean; + + constructor( + prefix: string = "NextChat MCP Client", + debugMode: boolean = false, + ) { + this.prefix = prefix; + this.debugMode = debugMode; + } + + info(message: any) { + this.log(colors.blue, message); + } + + success(message: any) { + this.log(colors.green, message); + } + + error(message: any) { + const formattedMessage = this.formatMessage(message); + console.error( + `${colors.red}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`, + ); + } + + warn(message: any) { + this.log(colors.yellow, message); + } + + debug(message: any) { + if (this.debugMode) { + this.log(colors.dim, message); + } + } + + private formatMessage(message: any): string { + return typeof message === "object" + ? JSON.stringify(message, null, 2) + : message; + } + + private log(color: string, message: any) { + const formattedMessage = this.formatMessage(message); + console.log( + `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`, + ); + } +} diff --git a/app/mcp/mcp_config.ts b/app/mcp/mcp_config.ts new file mode 100644 index 00000000000..044d04052a1 --- /dev/null +++ b/app/mcp/mcp_config.ts @@ -0,0 +1,40 @@ +export const MCP_CONF = { + "brave-search": { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-brave-search"], + env: { + BRAVE_API_KEY: "", + }, + }, + filesystem: { + command: "npx", + args: [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/Users/kadxy/Desktop", + ], + }, + github: { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-github"], + env: { + GITHUB_PERSONAL_ACCESS_TOKEN: "", + }, + }, + "google-maps": { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-google-maps"], + env: { + GOOGLE_MAPS_API_KEY: "", + }, + }, + "aws-kb-retrieval": { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-aws-kb-retrieval"], + env: { + AWS_ACCESS_KEY_ID: "", + AWS_SECRET_ACCESS_KEY: "", + AWS_REGION: "", + }, + }, +}; diff --git a/app/store/chat.ts b/app/store/chat.ts index 63d7394ece6..27d1f8620a3 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -29,6 +29,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config"; import { useAccessStore } from "./access"; import { collectModelsWithDefaultModel } from "../utils/model"; import { createEmptyMask, Mask } from "./mask"; +import { executeMcpAction } from "../mcp/actions"; const localStorage = safeLocalStorage(); @@ -425,9 +426,25 @@ export const useChatStore = createPersistStore( session.messages = session.messages.concat(); }); }, - onFinish(message) { + async onFinish(message) { botMessage.streaming = false; if (message) { + // console.log("[Bot Response] ", message); + const mcpMatch = message.match(/```json:mcp([\s\S]*?)```/); + if (mcpMatch) { + try { + const mcp = JSON.parse(mcpMatch[1]); + console.log("[MCP Request]", mcp); + + // 直接调用服务器端 action + const result = await executeMcpAction(mcp); + console.log("[MCP Response]", result); + } catch (error) { + console.error("[MCP Error]", error); + } + } else { + console.log("[MCP] No MCP found in response"); + } botMessage.content = message; botMessage.date = new Date().toLocaleString(); get().onNewMessage(botMessage, session); diff --git a/next.config.mjs b/next.config.mjs index 2bb6bc4f4b2..80241913929 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -32,6 +32,7 @@ const nextConfig = { }, experimental: { forceSwcTransforms: true, + serverActions: true, }, }; @@ -71,8 +72,10 @@ if (mode !== "export") { // }, { // https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions - source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*", - destination: "https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*", + source: + "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*", + destination: + "https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*", }, { source: "/api/proxy/google/:path*", @@ -99,7 +102,7 @@ if (mode !== "export") { destination: "https://dashscope.aliyuncs.com/api/:path*", }, ]; - + return { beforeFiles: ret, }; diff --git a/package.json b/package.json index e081567a4b1..a17f8ffa9cc 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@fortaine/fetch-event-source": "^3.0.6", "@hello-pangea/dnd": "^16.5.0", + "@modelcontextprotocol/sdk": "^1.0.4", "@next/third-parties": "^14.1.0", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", @@ -49,11 +50,12 @@ "remark-breaks": "^3.0.2", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", + "rt-client": "https://github.com/Azure-Samples/aoai-realtime-audio-sdk/releases/download/js/v0.5.0/rt-client-0.5.0.tgz", "sass": "^1.59.2", "spark-md5": "^3.0.2", "use-debounce": "^9.0.4", - "zustand": "^4.3.8", - "rt-client": "https://github.com/Azure-Samples/aoai-realtime-audio-sdk/releases/download/js/v0.5.0/rt-client-0.5.0.tgz" + "zod": "^3.24.1", + "zustand": "^4.3.8" }, "devDependencies": { "@tauri-apps/api": "^1.6.0", diff --git a/tsconfig.json b/tsconfig.json index c73eef3e876..6d24b42f1de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2015", + "target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, @@ -23,6 +23,6 @@ "@/*": ["./*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index dffc35e9cb7..138f3c8519b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1797,6 +1797,15 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@modelcontextprotocol/sdk@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.0.4.tgz#34ad1edd3db7dd7154e782312dfb29d2d0c11d21" + integrity sha512-C+jw1lF6HSGzs7EZpzHbXfzz9rj9him4BaoumlTciW/IDDgIpweF/qiCWKlP02QKg5PPcgY6xY2WCt5y2tpYow== + dependencies: + content-type "^1.0.5" + raw-body "^3.0.0" + zod "^3.23.8" + "@next/env@14.1.1": version "14.1.1" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.1.tgz#80150a8440eb0022a73ba353c6088d419b908bac" @@ -3039,6 +3048,11 @@ busboy@1.6.0: dependencies: streamsearch "^1.1.0" +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -3285,6 +3299,11 @@ concurrently@^8.2.2: tree-kill "^1.2.2" yargs "^17.7.2" +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -3849,6 +3868,11 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + dequal@^2.0.0, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -5007,6 +5031,17 @@ html-to-image@^1.11.11: resolved "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.11.tgz#c0f8a34dc9e4b97b93ff7ea286eb8562642ebbea" integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA== +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" @@ -5095,7 +5130,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7138,6 +7173,16 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -7569,6 +7614,11 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -7699,6 +7749,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" @@ -7977,6 +8032,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + tough-cookie@^4.1.2: version "4.1.4" resolved "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" @@ -8219,6 +8279,11 @@ universalify@^0.2.0: resolved "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + update-browserslist-db@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" @@ -8572,6 +8637,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod@^3.23.8, zod@^3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== + zustand@^4.3.8: version "4.3.8" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.8.tgz#37113df8e9e1421b0be1b2dca02b49b76210e7c4" From 664879b9df8c431664b06346962cff0319a3e85e Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sat, 28 Dec 2024 21:06:26 +0800 Subject: [PATCH 3/5] feat: Create all MCP Servers at startup --- .eslintignore | 3 +- app/mcp/actions.ts | 72 ++++++++++++++++++++++++++++++++-------- app/mcp/client.ts | 13 +++----- app/mcp/example.ts | 73 ++++------------------------------------- app/mcp/logger.ts | 29 +++++++++------- app/mcp/mcp_config.json | 16 +++++++++ app/mcp/mcp_config.ts | 40 ---------------------- app/page.tsx | 5 +-- app/store/chat.ts | 37 ++++++++++++--------- package.json | 3 +- yarn.lock | 8 ++--- 11 files changed, 134 insertions(+), 165 deletions(-) create mode 100644 app/mcp/mcp_config.json delete mode 100644 app/mcp/mcp_config.ts diff --git a/.eslintignore b/.eslintignore index 08975255475..8109e6bec48 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -public/serviceWorker.js \ No newline at end of file +public/serviceWorker.js +app/mcp/mcp_config.json \ No newline at end of file diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index 3d6ca4a68b8..af86834401b 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -2,32 +2,76 @@ import { createClient, executeRequest } from "./client"; import { MCPClientLogger } from "./logger"; -import { MCP_CONF } from "@/app/mcp/mcp_config"; +import conf from "./mcp_config.json"; const logger = new MCPClientLogger("MCP Server"); -let fsClient: any = null; +// Use Map to store all clients +const clientsMap = new Map(); -async function initFileSystemClient() { - if (!fsClient) { - fsClient = await createClient(MCP_CONF.filesystem, "fs"); - logger.success("FileSystem client initialized"); +// Whether initialized +let initialized = false; + +// Store failed clients +let errorClients: string[] = []; + +// Initialize all configured clients +export async function initializeMcpClients() { + // If already initialized, return + if (initialized) { + return; + } + + logger.info("Starting to initialize MCP clients..."); + + // Initialize all clients, key is clientId, value is client config + for (const [clientId, config] of Object.entries(conf.mcpServers)) { + try { + logger.info(`Initializing MCP client: ${clientId}`); + const client = await createClient(config, clientId); + clientsMap.set(clientId, client); + logger.success(`Client ${clientId} initialized`); + } catch (error) { + errorClients.push(clientId); + logger.error(`Failed to initialize client ${clientId}: ${error}`); + } } - return fsClient; -} -export async function executeMcpAction(request: any) { - "use server"; + initialized = true; + if (errorClients.length > 0) { + logger.warn(`Failed to initialize clients: ${errorClients.join(", ")}`); + } else { + logger.success("All MCP clients initialized"); + } + + const availableClients = await getAvailableClients(); + + logger.info(`Available clients: ${availableClients.join(",")}`); +} + +// Execute MCP request +export async function executeMcpAction(clientId: string, request: any) { try { - if (!fsClient) { - await initFileSystemClient(); + // Find the corresponding client + const client = clientsMap.get(clientId); + if (!client) { + logger.error(`Client ${clientId} not found`); + return; } - logger.info("Executing MCP request for fs"); - return await executeRequest(fsClient, request); + logger.info(`Executing MCP request for ${clientId}`); + // Execute request and return result + return await executeRequest(client, request); } catch (error) { logger.error(`MCP execution error: ${error}`); throw error; } } + +// Get all available client IDs +export async function getAvailableClients() { + return Array.from(clientsMap.keys()).filter( + (clientId) => !errorClients.includes(clientId), + ); +} diff --git a/app/mcp/client.ts b/app/mcp/client.ts index d71314f3ac9..7eb55fb8222 100644 --- a/app/mcp/client.ts +++ b/app/mcp/client.ts @@ -29,11 +29,9 @@ export async function createClient( }, { capabilities: { - roots: { - // listChanged indicates whether the client will emit notifications when the list of roots changes. - // listChanged 指示客户端在根列表更改时是否发出通知。 - listChanged: true, - }, + // roots: { + // listChanged: true, + // }, }, }, ); @@ -80,8 +78,7 @@ export async function listPrimitives(client: Client) { return primitives; } +/** Execute a request */ export async function executeRequest(client: Client, request: any) { - const r = client.request(request, z.any()); - console.log(r); - return r; + return client.request(request, z.any()); } diff --git a/app/mcp/example.ts b/app/mcp/example.ts index d924ba66470..83fc8784cf6 100644 --- a/app/mcp/example.ts +++ b/app/mcp/example.ts @@ -1,35 +1,16 @@ import { createClient, listPrimitives } from "@/app/mcp/client"; import { MCPClientLogger } from "@/app/mcp/logger"; -import { z } from "zod"; -import { MCP_CONF } from "@/app/mcp/mcp_config"; +import conf from "./mcp_config.json"; -const logger = new MCPClientLogger("MCP FS Example", true); - -const ListAllowedDirectoriesResultSchema = z.object({ - content: z.array( - z.object({ - type: z.string(), - text: z.string(), - }), - ), -}); - -const ReadFileResultSchema = z.object({ - content: z.array( - z.object({ - type: z.string(), - text: z.string(), - }), - ), -}); +const logger = new MCPClientLogger("MCP Server Example", true); async function main() { logger.info("Connecting to server..."); - const client = await createClient(MCP_CONF.filesystem, "fs"); + const client = await createClient(conf.mcpServers.everything, "everything"); const primitives = await listPrimitives(client); - logger.success(`Connected to server fs`); + logger.success(`Connected to server everything`); logger.info( `server capabilities: ${Object.keys( @@ -37,53 +18,11 @@ async function main() { ).join(", ")}`, ); - logger.debug("Server supports the following primitives:"); + logger.info("Server supports the following primitives:"); primitives.forEach((primitive) => { - logger.debug("\n" + JSON.stringify(primitive, null, 2)); + logger.info("\n" + JSON.stringify(primitive, null, 2)); }); - - const listAllowedDirectories = async () => { - const result = await client.request( - { - method: "tools/call", - params: { - name: "list_allowed_directories", - arguments: {}, - }, - }, - ListAllowedDirectoriesResultSchema, - ); - logger.success(`Allowed directories: ${result.content[0].text}`); - return result; - }; - - const readFile = async (path: string) => { - const result = await client.request( - { - method: "tools/call", - params: { - name: "read_file", - arguments: { - path: path, - }, - }, - }, - ReadFileResultSchema, - ); - logger.success(`File contents for ${path}:\n${result.content[0].text}`); - return result; - }; - - try { - logger.info("Example 1: List allowed directories\n"); - await listAllowedDirectories(); - - logger.info("\nExample 2: Read a file\n"); - await readFile("/users/kadxy/desktop/test.txt"); - } catch (error) { - logger.error(`Error executing examples: ${error}`); - } } main().catch((error) => { diff --git a/app/mcp/logger.ts b/app/mcp/logger.ts index a39304afe91..25129c592c3 100644 --- a/app/mcp/logger.ts +++ b/app/mcp/logger.ts @@ -1,3 +1,4 @@ +// ANSI color codes for terminal output const colors = { reset: "\x1b[0m", bright: "\x1b[1m", @@ -21,40 +22,44 @@ export class MCPClientLogger { } info(message: any) { - this.log(colors.blue, message); + this.print(colors.blue, message); } success(message: any) { - this.log(colors.green, message); + this.print(colors.green, message); } error(message: any) { - const formattedMessage = this.formatMessage(message); - console.error( - `${colors.red}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`, - ); + this.print(colors.red, message); } warn(message: any) { - this.log(colors.yellow, message); + this.print(colors.yellow, message); } debug(message: any) { if (this.debugMode) { - this.log(colors.dim, message); + this.print(colors.dim, message); } } + /** + * Format message to string, if message is object, convert to JSON string + */ private formatMessage(message: any): string { return typeof message === "object" ? JSON.stringify(message, null, 2) : message; } - private log(color: string, message: any) { + /** + * Print formatted message to console + */ + private print(color: string, message: any) { const formattedMessage = this.formatMessage(message); - console.log( - `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`, - ); + const logMessage = `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`; + + // 只使用 console.log,这样日志会显示在 Tauri 的终端中 + console.log(logMessage); } } diff --git a/app/mcp/mcp_config.json b/app/mcp/mcp_config.json new file mode 100644 index 00000000000..6ad18236b52 --- /dev/null +++ b/app/mcp/mcp_config.json @@ -0,0 +1,16 @@ +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "/Users/kadxy/Desktop" + ] + }, + "everything": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-everything"] + } + } +} diff --git a/app/mcp/mcp_config.ts b/app/mcp/mcp_config.ts deleted file mode 100644 index 044d04052a1..00000000000 --- a/app/mcp/mcp_config.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const MCP_CONF = { - "brave-search": { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-brave-search"], - env: { - BRAVE_API_KEY: "", - }, - }, - filesystem: { - command: "npx", - args: [ - "-y", - "@modelcontextprotocol/server-filesystem", - "/Users/kadxy/Desktop", - ], - }, - github: { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-github"], - env: { - GITHUB_PERSONAL_ACCESS_TOKEN: "", - }, - }, - "google-maps": { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-google-maps"], - env: { - GOOGLE_MAPS_API_KEY: "", - }, - }, - "aws-kb-retrieval": { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-aws-kb-retrieval"], - env: { - AWS_ACCESS_KEY_ID: "", - AWS_SECRET_ACCESS_KEY: "", - AWS_REGION: "", - }, - }, -}; diff --git a/app/page.tsx b/app/page.tsx index b3f169a9b74..d4ba2a27613 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,13 @@ import { Analytics } from "@vercel/analytics/react"; - import { Home } from "./components/home"; - import { getServerSideConfig } from "./config/server"; +import { initializeMcpClients } from "./mcp/actions"; const serverConfig = getServerSideConfig(); export default async function App() { + await initializeMcpClients(); + return ( <> diff --git a/app/store/chat.ts b/app/store/chat.ts index 27d1f8620a3..3444bb43635 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -356,6 +356,27 @@ export const useChatStore = createPersistStore( onNewMessage(message: ChatMessage, targetSession: ChatSession) { get().updateTargetSession(targetSession, (session) => { + // Check and process MCP JSON + const content = + typeof message.content === "string" ? message.content : ""; + const mcpMatch = content.match(/```json:mcp:(\w+)([\s\S]*?)```/); + if (mcpMatch) { + try { + const clientId = mcpMatch[1]; + const mcp = JSON.parse(mcpMatch[2]); + console.log("[MCP Request]", clientId, mcp); + // Execute MCP action + executeMcpAction(clientId, mcp) + .then((result) => { + console.log("[MCP Response]", result); + }) + .catch((error) => { + console.error("[MCP Error]", error); + }); + } catch (error) { + console.error("[MCP Error]", error); + } + } session.messages = session.messages.concat(); session.lastUpdate = Date.now(); }); @@ -429,22 +450,6 @@ export const useChatStore = createPersistStore( async onFinish(message) { botMessage.streaming = false; if (message) { - // console.log("[Bot Response] ", message); - const mcpMatch = message.match(/```json:mcp([\s\S]*?)```/); - if (mcpMatch) { - try { - const mcp = JSON.parse(mcpMatch[1]); - console.log("[MCP Request]", mcp); - - // 直接调用服务器端 action - const result = await executeMcpAction(mcp); - console.log("[MCP Response]", result); - } catch (error) { - console.error("[MCP Error]", error); - } - } else { - console.log("[MCP] No MCP found in response"); - } botMessage.content = message; botMessage.date = new Date().toLocaleString(); get().onNewMessage(botMessage, session); diff --git a/package.json b/package.json index a17f8ffa9cc..0efe27b391a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"", "app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"", "app:build": "yarn mask && yarn tauri build", + "app:clear": "yarn tauri dev", "prompts": "node ./scripts/fetch-prompts.mjs", "prepare": "husky install", "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev", @@ -58,7 +59,7 @@ "zustand": "^4.3.8" }, "devDependencies": { - "@tauri-apps/api": "^1.6.0", + "@tauri-apps/api": "^2.1.1", "@tauri-apps/cli": "1.5.11", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/yarn.lock b/yarn.lock index 138f3c8519b..5b9741b2b4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2038,10 +2038,10 @@ dependencies: tslib "^2.4.0" -"@tauri-apps/api@^1.6.0": - version "1.6.0" - resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz#745b7e4e26782c3b2ad9510d558fa5bb2cf29186" - integrity sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg== +"@tauri-apps/api@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.1.1.tgz#77d4ddb683d31072de4e6a47c8613d9db011652b" + integrity sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A== "@tauri-apps/cli-darwin-arm64@1.5.11": version "1.5.11" From e1ba8f1b0f122a73194b2f3716fdb78173647e05 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sun, 29 Dec 2024 08:29:02 +0800 Subject: [PATCH 4/5] feat: Send MCP response as a user --- app/mcp/utils.ts | 11 ++++++++++ app/store/chat.ts | 52 ++++++++++++++++++++++++++++------------------- 2 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 app/mcp/utils.ts diff --git a/app/mcp/utils.ts b/app/mcp/utils.ts new file mode 100644 index 00000000000..5b6dcbf027f --- /dev/null +++ b/app/mcp/utils.ts @@ -0,0 +1,11 @@ +export function isMcpJson(content: string) { + return content.match(/```json:mcp:(\w+)([\s\S]*?)```/); +} + +export function extractMcpJson(content: string) { + const match = content.match(/```json:mcp:(\w+)([\s\S]*?)```/); + if (match) { + return { clientId: match[1], mcp: JSON.parse(match[2]) }; + } + return null; +} diff --git a/app/store/chat.ts b/app/store/chat.ts index 3444bb43635..d30fa1fea48 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -30,6 +30,7 @@ import { useAccessStore } from "./access"; import { collectModelsWithDefaultModel } from "../utils/model"; import { createEmptyMask, Mask } from "./mask"; import { executeMcpAction } from "../mcp/actions"; +import { extractMcpJson, isMcpJson } from "../mcp/utils"; const localStorage = safeLocalStorage(); @@ -356,31 +357,14 @@ export const useChatStore = createPersistStore( onNewMessage(message: ChatMessage, targetSession: ChatSession) { get().updateTargetSession(targetSession, (session) => { - // Check and process MCP JSON - const content = - typeof message.content === "string" ? message.content : ""; - const mcpMatch = content.match(/```json:mcp:(\w+)([\s\S]*?)```/); - if (mcpMatch) { - try { - const clientId = mcpMatch[1]; - const mcp = JSON.parse(mcpMatch[2]); - console.log("[MCP Request]", clientId, mcp); - // Execute MCP action - executeMcpAction(clientId, mcp) - .then((result) => { - console.log("[MCP Response]", result); - }) - .catch((error) => { - console.error("[MCP Error]", error); - }); - } catch (error) { - console.error("[MCP Error]", error); - } - } session.messages = session.messages.concat(); session.lastUpdate = Date.now(); }); + get().updateStat(message, targetSession); + + get().checkMcpJson(message); + get().summarizeSession(false, targetSession); }, @@ -786,6 +770,32 @@ export const useChatStore = createPersistStore( lastInput, }); }, + checkMcpJson(message: ChatMessage) { + const content = + typeof message.content === "string" ? message.content : ""; + if (isMcpJson(content)) { + try { + const mcpRequest = extractMcpJson(content); + if (mcpRequest) { + console.debug("[MCP Request]", mcpRequest); + + executeMcpAction(mcpRequest.clientId, mcpRequest.mcp) + .then((result) => { + console.log("[MCP Response]", result); + // 直接使用onUserInput发送结果 + get().onUserInput( + typeof result === "object" + ? JSON.stringify(result) + : String(result), + ); + }) + .catch((error) => showToast(String(error))); + } + } catch (error) { + console.error("[MCP Error]", error); + } + } + }, }; return methods; From fe67f79050c7f4b8971f9b9aabc22c5fd23bac07 Mon Sep 17 00:00:00 2001 From: Kadxy <2230318258@qq.com> Date: Sun, 29 Dec 2024 09:24:52 +0800 Subject: [PATCH 5/5] feat: MCP message type --- app/mcp/actions.ts | 9 +++++-- app/mcp/client.ts | 6 ++++- app/mcp/types.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++++ app/store/chat.ts | 48 ++++++++++++++++++++++-------------- 4 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 app/mcp/types.ts diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts index af86834401b..5fe611b3a84 100644 --- a/app/mcp/actions.ts +++ b/app/mcp/actions.ts @@ -3,8 +3,9 @@ import { createClient, executeRequest } from "./client"; import { MCPClientLogger } from "./logger"; import conf from "./mcp_config.json"; +import { McpRequestMessage } from "./types"; -const logger = new MCPClientLogger("MCP Server"); +const logger = new MCPClientLogger("MCP Actions"); // Use Map to store all clients const clientsMap = new Map(); @@ -51,7 +52,10 @@ export async function initializeMcpClients() { } // Execute MCP request -export async function executeMcpAction(clientId: string, request: any) { +export async function executeMcpAction( + clientId: string, + request: McpRequestMessage, +) { try { // Find the corresponding client const client = clientsMap.get(clientId); @@ -61,6 +65,7 @@ export async function executeMcpAction(clientId: string, request: any) { } logger.info(`Executing MCP request for ${clientId}`); + // Execute request and return result return await executeRequest(client, request); } catch (error) { diff --git a/app/mcp/client.ts b/app/mcp/client.ts index 7eb55fb8222..0600f00be92 100644 --- a/app/mcp/client.ts +++ b/app/mcp/client.ts @@ -1,6 +1,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { MCPClientLogger } from "./logger"; +import { McpRequestMessage } from "./types"; import { z } from "zod"; export interface ServerConfig { @@ -79,6 +80,9 @@ export async function listPrimitives(client: Client) { } /** Execute a request */ -export async function executeRequest(client: Client, request: any) { +export async function executeRequest( + client: Client, + request: McpRequestMessage, +) { return client.request(request, z.any()); } diff --git a/app/mcp/types.ts b/app/mcp/types.ts new file mode 100644 index 00000000000..763121bad88 --- /dev/null +++ b/app/mcp/types.ts @@ -0,0 +1,61 @@ +// ref: https://spec.modelcontextprotocol.io/specification/basic/messages/ + +import { z } from "zod"; + +export interface McpRequestMessage { + jsonrpc?: "2.0"; + id?: string | number; + method: "tools/call" | string; + params?: { + [key: string]: unknown; + }; +} + +export const McpRequestMessageSchema: z.ZodType = z.object({ + jsonrpc: z.literal("2.0").optional(), + id: z.union([z.string(), z.number()]).optional(), + method: z.string(), + params: z.record(z.unknown()).optional(), +}); + +export interface McpResponseMessage { + jsonrpc?: "2.0"; + id?: string | number; + result?: { + [key: string]: unknown; + }; + error?: { + code: number; + message: string; + data?: unknown; + }; +} + +export const McpResponseMessageSchema: z.ZodType = z.object( + { + jsonrpc: z.literal("2.0").optional(), + id: z.union([z.string(), z.number()]).optional(), + result: z.record(z.unknown()).optional(), + error: z + .object({ + code: z.number(), + message: z.string(), + data: z.unknown().optional(), + }) + .optional(), + }, +); + +export interface McpNotifications { + jsonrpc?: "2.0"; + method: string; + params?: { + [key: string]: unknown; + }; +} + +export const McpNotificationsSchema: z.ZodType = z.object({ + jsonrpc: z.literal("2.0").optional(), + method: z.string(), + params: z.record(z.unknown()).optional(), +}); diff --git a/app/store/chat.ts b/app/store/chat.ts index d30fa1fea48..e0ee956219c 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -1,4 +1,9 @@ -import { getMessageTextContent, trimTopic } from "../utils"; +import { + getMessageTextContent, + isDalle3, + safeLocalStorage, + trimTopic, +} from "../utils"; import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; import { nanoid } from "nanoid"; @@ -14,14 +19,13 @@ import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, DEFAULT_SYSTEM_TEMPLATE, + GEMINI_SUMMARIZE_MODEL, KnowledgeCutOffDate, + ServiceProvider, StoreKey, SUMMARIZE_MODEL, - GEMINI_SUMMARIZE_MODEL, - ServiceProvider, } from "../constant"; import Locale, { getLang } from "../locales"; -import { isDalle3, safeLocalStorage } from "../utils"; import { prettyObject } from "../utils/format"; import { createPersistStore } from "../utils/store"; import { estimateTokenLength } from "../utils/token"; @@ -55,6 +59,7 @@ export type ChatMessage = RequestMessage & { model?: ModelType; tools?: ChatMessageTool[]; audio_url?: string; + isMcpResponse?: boolean; }; export function createMessage(override: Partial): ChatMessage { @@ -368,20 +373,22 @@ export const useChatStore = createPersistStore( get().summarizeSession(false, targetSession); }, - async onUserInput(content: string, attachImages?: string[]) { + async onUserInput( + content: string, + attachImages?: string[], + isMcpResponse?: boolean, + ) { const session = get().currentSession(); const modelConfig = session.mask.modelConfig; - const userContent = fillTemplateWith(content, modelConfig); - console.log("[User Input] after template: ", userContent); - - let mContent: string | MultimodalContent[] = userContent; + // MCP Response no need to fill template + let mContent: string | MultimodalContent[] = isMcpResponse + ? content + : fillTemplateWith(content, modelConfig); - if (attachImages && attachImages.length > 0) { + if (!isMcpResponse && attachImages && attachImages.length > 0) { mContent = [ - ...(userContent - ? [{ type: "text" as const, text: userContent }] - : []), + ...(content ? [{ type: "text" as const, text: content }] : []), ...attachImages.map((url) => ({ type: "image_url" as const, image_url: { url }, @@ -392,6 +399,7 @@ export const useChatStore = createPersistStore( let userMessage: ChatMessage = createMessage({ role: "user", content: mContent, + isMcpResponse, }); const botMessage: ChatMessage = createMessage({ @@ -770,9 +778,10 @@ export const useChatStore = createPersistStore( lastInput, }); }, + + /** check if the message contains MCP JSON and execute the MCP action */ checkMcpJson(message: ChatMessage) { - const content = - typeof message.content === "string" ? message.content : ""; + const content = getMessageTextContent(message); if (isMcpJson(content)) { try { const mcpRequest = extractMcpJson(content); @@ -782,11 +791,14 @@ export const useChatStore = createPersistStore( executeMcpAction(mcpRequest.clientId, mcpRequest.mcp) .then((result) => { console.log("[MCP Response]", result); - // 直接使用onUserInput发送结果 - get().onUserInput( + const mcpResponse = typeof result === "object" ? JSON.stringify(result) - : String(result), + : String(result); + get().onUserInput( + `\`\`\`json:mcp:${mcpRequest.clientId}\n${mcpResponse}\n\`\`\``, + [], + true, ); }) .catch((error) => showToast(String(error)));