From ca8adbbf95345bc46a521be83a8079215f846180 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Mon, 28 Apr 2025 12:45:51 +0800 Subject: [PATCH] feat: streamable http mcp (#4695) * feat: streamable http mcp * mcp api path * fix: ts --- packages/service/package.json | 2 +- packages/web/i18n/en/dashboard_mcp.json | 2 + packages/web/i18n/zh-CN/dashboard_mcp.json | 2 + packages/web/i18n/zh-Hant/dashboard_mcp.json | 2 + pnpm-lock.yaml | 96 +----- projects/app/package.json | 2 +- .../pageComponents/dashboard/Container.tsx | 16 +- .../pageComponents/dashboard/mcp/usageWay.tsx | 120 +++++-- .../app/src/pages/api/mcp/app/[key]/mcp.ts | 115 +++++++ .../pages/api/support/mcp/server/toolCall.ts | 188 +---------- .../pages/api/support/mcp/server/toolList.ts | 140 +------- .../app/src/service/support/mcp/type.d.ts | 5 + projects/app/src/service/support/mcp/utils.ts | 319 ++++++++++++++++++ projects/mcp_server/package.json | 2 +- .../api/support/mcp/server/toolList.test.ts | 8 +- 15 files changed, 562 insertions(+), 457 deletions(-) create mode 100644 projects/app/src/pages/api/mcp/app/[key]/mcp.ts create mode 100644 projects/app/src/service/support/mcp/type.d.ts create mode 100644 projects/app/src/service/support/mcp/utils.ts diff --git a/packages/service/package.json b/packages/service/package.json index 0458ce8c8..fe7b28a18 100644 --- a/packages/service/package.json +++ b/packages/service/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "dependencies": { "@fastgpt/global": "workspace:*", - "@modelcontextprotocol/sdk": "^1.10.0", + "@modelcontextprotocol/sdk": "^1.10.2", "@node-rs/jieba": "2.0.1", "@xmldom/xmldom": "^0.8.10", "@zilliz/milvus2-sdk-node": "2.4.2", diff --git a/packages/web/i18n/en/dashboard_mcp.json b/packages/web/i18n/en/dashboard_mcp.json index 0624471aa..18dca9c1d 100644 --- a/packages/web/i18n/en/dashboard_mcp.json +++ b/packages/web/i18n/en/dashboard_mcp.json @@ -12,9 +12,11 @@ "mcp_apps": "Number of associated applications", "mcp_endpoints": "Access address", "mcp_json_config": "Access script", + "mcp_link_way": "Access method", "mcp_name": "MCP service name", "mcp_server": "MCP Services", "mcp_server_description": "Allows you to select some applications to provide external use with the MCP protocol. \nDue to the immaturity of the MCP protocol, this feature is still in the beta stage.", + "not_sse_server": "The system does not configure SSE access service", "search_app": "Search for apps", "select_app": "Application selection", "start_use": "Get started", diff --git a/packages/web/i18n/zh-CN/dashboard_mcp.json b/packages/web/i18n/zh-CN/dashboard_mcp.json index 02c49b6f3..e759c61f5 100644 --- a/packages/web/i18n/zh-CN/dashboard_mcp.json +++ b/packages/web/i18n/zh-CN/dashboard_mcp.json @@ -12,9 +12,11 @@ "mcp_apps": "关联应用数量", "mcp_endpoints": "接入地址", "mcp_json_config": "接入脚本", + "mcp_link_way": "接入方式", "mcp_name": "MCP 服务名", "mcp_server": "MCP 服务", "mcp_server_description": "允许你选择部分应用,以 MCP 的协议对外提供使用。由于 MCP 协议的不成熟,该功能仍处于测试阶段。", + "not_sse_server": "系统未配置 SSE 接入服务", "search_app": "搜索应用", "select_app": "应用选择", "start_use": "开始使用", diff --git a/packages/web/i18n/zh-Hant/dashboard_mcp.json b/packages/web/i18n/zh-Hant/dashboard_mcp.json index 1de5466ee..da5d98d96 100644 --- a/packages/web/i18n/zh-Hant/dashboard_mcp.json +++ b/packages/web/i18n/zh-Hant/dashboard_mcp.json @@ -12,9 +12,11 @@ "mcp_apps": "關聯應用數量", "mcp_endpoints": "接入地址", "mcp_json_config": "接入腳本", + "mcp_link_way": "接入方式", "mcp_name": "MCP 服務名", "mcp_server": "MCP 服務", "mcp_server_description": "允許你選擇部分應用,以 MCP 的協議對外提供使用。\n由於 MCP 協議的不成熟,該功能仍處於測試階段。", + "not_sse_server": "系統未配置 SSE 接入服務", "search_app": "搜索應用", "select_app": "應用選擇", "start_use": "開始使用", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7a5411a7..bfb83f055 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,7 @@ importers: version: 5.1.3 next: specifier: 14.2.26 - version: 14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + version: 14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) openai: specifier: 4.61.0 version: 4.61.0(encoding@0.1.13)(zod@3.24.2) @@ -164,7 +164,7 @@ importers: specifier: workspace:* version: link:../global '@modelcontextprotocol/sdk': - specifier: ^1.10.0 + specifier: ^1.10.2 version: 1.10.2 '@node-rs/jieba': specifier: 2.0.1 @@ -246,7 +246,7 @@ importers: version: 3.13.0 next: specifier: 14.2.26 - version: 14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + version: 14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) nextjs-cors: specifier: ^2.2.0 version: 2.2.0(next@14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)) @@ -455,7 +455,7 @@ importers: version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1) '@chakra-ui/next-js': specifier: 2.4.2 - version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) + version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1) '@chakra-ui/react': specifier: 2.10.7 version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -493,7 +493,7 @@ importers: specifier: ^3.0.6 version: 3.0.6 '@modelcontextprotocol/sdk': - specifier: ^1.10.0 + specifier: ^1.10.2 version: 1.10.2 '@node-rs/jieba': specifier: 2.0.1 @@ -560,7 +560,7 @@ importers: version: 14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) next-i18next: specifier: 15.4.2 - version: 15.4.2(i18next@23.16.8)(next@14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 15.4.2(i18next@23.16.8)(next@14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -677,8 +677,8 @@ importers: projects/mcp_server: dependencies: '@modelcontextprotocol/sdk': - specifier: 1.9.0 - version: 1.9.0 + specifier: ^1.10.2 + version: 1.10.2 axios: specifier: ^1.8.2 version: 1.8.4 @@ -2369,10 +2369,6 @@ packages: resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==} engines: {node: '>=18'} - '@modelcontextprotocol/sdk@1.9.0': - resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==} - engines: {node: '>=18'} - '@monaco-editor/loader@1.5.0': resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} @@ -11012,20 +11008,12 @@ snapshots: '@chakra-ui/system': 2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1) react: 18.3.1 - '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)': - dependencies: - '@chakra-ui/react': 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@emotion/cache': 11.14.0 - '@emotion/react': 11.11.1(@types/react@18.3.1)(react@18.3.1) - next: 14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) - react: 18.3.1 - '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)': dependencies: '@chakra-ui/react': 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@emotion/cache': 11.14.0 '@emotion/react': 11.11.1(@types/react@18.3.1)(react@18.3.1) - next: 14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) react: 18.3.1 '@chakra-ui/object-utils@2.1.0': {} @@ -11909,21 +11897,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@1.9.0': - dependencies: - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.6 - express: 5.1.0 - express-rate-limit: 7.5.0(express@5.1.0) - pkce-challenge: 5.0.0 - raw-body: 3.0.0 - zod: 3.24.2 - zod-to-json-schema: 3.24.5(zod@3.24.2) - transitivePeerDependencies: - - supports-color - '@monaco-editor/loader@1.5.0': dependencies: state-local: 1.0.7 @@ -13705,7 +13678,7 @@ snapshots: axios@1.8.3: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.9(debug@4.4.0) form-data: 4.0.2 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -15667,8 +15640,6 @@ snapshots: dependencies: tslib: 2.8.1 - follow-redirects@1.15.9: {} - follow-redirects@1.15.9(debug@4.4.0): optionalDependencies: debug: 4.4.0(supports-color@5.5.0) @@ -18121,18 +18092,6 @@ snapshots: transitivePeerDependencies: - supports-color - next-i18next@15.4.2(i18next@23.16.8)(next@14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): - dependencies: - '@babel/runtime': 7.26.10 - '@types/hoist-non-react-statics': 3.3.6 - core-js: 3.41.0 - hoist-non-react-statics: 3.3.2 - i18next: 23.16.8 - i18next-fs-backend: 2.6.0 - next: 14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) - react: 18.3.1 - react-i18next: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - next-i18next@15.4.2(i18next@23.16.8)(next@14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.10 @@ -18141,7 +18100,7 @@ snapshots: hoist-non-react-statics: 3.3.2 i18next: 23.16.8 i18next-fs-backend: 2.6.0 - next: 14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) react: 18.3.1 react-i18next: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -18171,36 +18130,10 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1): - dependencies: - '@next/env': 14.2.26 - '@swc/helpers': 0.5.5 - busboy: 1.6.0 - caniuse-lite: 1.0.30001704 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(react@18.3.1) - optionalDependencies: - '@next/swc-darwin-arm64': 14.2.26 - '@next/swc-darwin-x64': 14.2.26 - '@next/swc-linux-arm64-gnu': 14.2.26 - '@next/swc-linux-arm64-musl': 14.2.26 - '@next/swc-linux-x64-gnu': 14.2.26 - '@next/swc-linux-x64-musl': 14.2.26 - '@next/swc-win32-arm64-msvc': 14.2.26 - '@next/swc-win32-ia32-msvc': 14.2.26 - '@next/swc-win32-x64-msvc': 14.2.26 - sass: 1.85.1 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - nextjs-cors@2.2.0(next@14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)): dependencies: cors: 2.8.5 - next: 14.2.26(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) node-abi@3.74.0: dependencies: @@ -19880,11 +19813,6 @@ snapshots: optionalDependencies: '@babel/core': 7.26.10 - styled-jsx@5.1.1(react@18.3.1): - dependencies: - client-only: 0.0.1 - react: 18.3.1 - stylis@4.2.0: {} stylis@4.3.6: {} diff --git a/projects/app/package.json b/projects/app/package.json index 0d5da4565..9c490b844 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -24,7 +24,7 @@ "@fastgpt/templates": "workspace:*", "@fastgpt/web": "workspace:*", "@fortaine/fetch-event-source": "^3.0.6", - "@modelcontextprotocol/sdk": "^1.10.0", + "@modelcontextprotocol/sdk": "^1.10.2", "@node-rs/jieba": "2.0.1", "@tanstack/react-query": "^4.24.10", "ahooks": "^3.7.11", diff --git a/projects/app/src/pageComponents/dashboard/Container.tsx b/projects/app/src/pageComponents/dashboard/Container.tsx index 59f65b918..4e862bd4c 100644 --- a/projects/app/src/pageComponents/dashboard/Container.tsx +++ b/projects/app/src/pageComponents/dashboard/Container.tsx @@ -184,16 +184,12 @@ const DashboardContainer = ({ : []) ] }, - ...(feConfigs?.mcpServerProxyEndpoint - ? [ - { - groupId: TabEnum.mcp_server, - groupAvatar: 'key', - groupName: t('common:mcp_server'), - children: [] - } - ] - : []) + { + groupId: TabEnum.mcp_server, + groupAvatar: 'key', + groupName: t('common:mcp_server'), + children: [] + } ]; }, [currentType, feConfigs.appTemplateCourse, pluginGroups, t, templateList, templateTags]); diff --git a/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx b/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx index 579e67035..7d8418d4a 100644 --- a/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx +++ b/projects/app/src/pageComponents/dashboard/mcp/usageWay.tsx @@ -1,59 +1,113 @@ import { McpKeyType } from '@fastgpt/global/support/mcp/type'; import MyModal from '@fastgpt/web/components/common/MyModal'; -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'next-i18next'; import { Box, Flex, HStack, ModalBody } from '@chakra-ui/react'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; import MyIconButton from '@fastgpt/web/components/common/Icon/button'; +import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; + +type LinkWay = 'sse' | 'http'; const UsageWay = ({ mcp, onClose }: { mcp: McpKeyType; onClose: () => void }) => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); + const [linkWay, setLinkWay] = useState('http'); - const sseUrl = `${feConfigs?.mcpServerProxyEndpoint}/${mcp.key}/sse`; - const jsonConfig = `{ + const { url, jsonConfig } = (() => { + if (linkWay === 'http') { + const baseUrl = feConfigs?.customApiDomain || `${location.origin}/api`; + const url = `${baseUrl}/mcp/app/${mcp.key}/mcp`; + const jsonConfig = `{ "mcpServers": { "${feConfigs?.systemTitle}-mcp-${mcp._id}": { - "url": "${sseUrl}" + "url": "${url}" + } + } +}`; + return { + url, + jsonConfig + }; + } + + const url = feConfigs?.mcpServerProxyEndpoint + ? `${feConfigs?.mcpServerProxyEndpoint}/${mcp.key}/sse` + : ''; + const jsonConfig = `{ + "mcpServers": { + "${feConfigs?.systemTitle}-mcp-${mcp._id}": { + "url": "${url}" } } }`; + return { + url, + jsonConfig + }; + })(); + return ( - + - - {t('dashboard_mcp:mcp_endpoints')} - - - {sseUrl} + + + m={'auto'} + w={'100%'} + list={[ + { label: 'Streamable HTTP', value: 'http' }, + { label: 'SSE', value: 'sse' } + ]} + value={linkWay} + onChange={setLinkWay} + /> + + {url ? ( + <> + + {t('dashboard_mcp:mcp_endpoints')} + + + {url} + + + + + - - - - - - - - - {t('dashboard_mcp:mcp_json_config')} - - - - - - {jsonConfig} + + + + {t('dashboard_mcp:mcp_json_config')} + + + + + + {jsonConfig} + + - - + + ) : ( + + {t('dashboard_mcp:not_sse_server')} + + )} ); diff --git a/projects/app/src/pages/api/mcp/app/[key]/mcp.ts b/projects/app/src/pages/api/mcp/app/[key]/mcp.ts new file mode 100644 index 000000000..5d27ceb41 --- /dev/null +++ b/projects/app/src/pages/api/mcp/app/[key]/mcp.ts @@ -0,0 +1,115 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { addLog } from '@fastgpt/service/common/system/log'; +import { + CallToolRequestSchema, + CallToolResult, + ListToolsRequestSchema +} from '@modelcontextprotocol/sdk/types'; +import { callMcpServerTool, getMcpServerTools } from '@/service/support/mcp/utils'; +import { toolCallProps } from '@/service/support/mcp/type'; +import { getErrText } from '@fastgpt/global/common/error/utils'; + +export type mcpQuery = { key: string }; + +export type mcpBody = toolCallProps; + +const handlePost = async (req: ApiRequestProps, res: ApiResponseType) => { + const key = req.query.key; + const server = new Server( + { + name: 'fastgpt-mcp-server-http-streamable', + version: '1.0.0' + }, + { + capabilities: { + tools: {} + } + } + ); + const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined + }); + res.on('close', () => { + addLog.debug('[MCP server] Close connection'); + transport.close(); + server.close(); + }); + + try { + const tools = await getMcpServerTools(key); + // Register list tools + server.setRequestHandler(ListToolsRequestSchema, () => ({ + tools + })); + + // Register call tool + const handleToolCall = async ( + name: string, + args: Record + ): Promise => { + try { + addLog.debug(`Call tool: ${name} with args: ${JSON.stringify(args)}`); + const result = await callMcpServerTool({ key, toolName: name, inputs: args }); + + return { + content: [ + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result) + } + ], + isError: false + }; + } catch (error) { + return { + message: getErrText(error), + content: [], + isError: true + }; + } + }; + server.setRequestHandler(CallToolRequestSchema, async (request) => { + return handleToolCall(request.params.name, request.params.arguments ?? {}); + }); + + // Connect to transport + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + addLog.error('[MCP server] Error handling MCP request:', error); + if (!res.writableFinished) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error' + }, + id: null + }); + } + } +}; + +async function handler(req: ApiRequestProps, res: ApiResponseType) { + const method = req.method; + + if (method === 'POST') { + return handlePost(req, res); + } + + res.writeHead(405).end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed.' + }, + id: null + }) + ); + return; +} + +export default handler; diff --git a/projects/app/src/pages/api/support/mcp/server/toolCall.ts b/projects/app/src/pages/api/support/mcp/server/toolCall.ts index e3656123b..3c04b974d 100644 --- a/projects/app/src/pages/api/support/mcp/server/toolCall.ts +++ b/projects/app/src/pages/api/support/mcp/server/toolCall.ts @@ -1,199 +1,19 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; -import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; -import { MongoApp } from '@fastgpt/service/core/app/schema'; -import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; -import { AppSchema } from '@fastgpt/global/core/app/type'; -import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team'; -import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; -import { getNanoid } from '@fastgpt/global/common/string/tools'; -import { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type'; -import { - getPluginRunUserQuery, - updatePluginInputByVariables -} from '@fastgpt/global/core/workflow/utils'; -import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils'; -import { - ChatItemValueTypeEnum, - ChatRoleEnum, - ChatSourceEnum -} from '@fastgpt/global/core/chat/constants'; -import { - getWorkflowEntryNodeIds, - storeEdges2RuntimeEdges, - storeNodes2RuntimeNodes -} from '@fastgpt/global/core/workflow/runtime/utils'; -import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; -import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; -import { getChatTitleFromChatMessage, removeEmptyUserInput } from '@fastgpt/global/core/chat/utils'; -import { saveChat } from '@fastgpt/service/core/chat/saveChat'; -import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; -import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller'; -import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; -import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { toolCallProps } from '@/service/support/mcp/type'; +import { callMcpServerTool } from '@/service/support/mcp/utils'; export type toolCallQuery = {}; -export type toolCallBody = { - key: string; - toolName: string; - inputs: Record; -}; +export type toolCallBody = toolCallProps; export type toolCallResponse = {}; -const dispatchApp = async (app: AppSchema, variables: Record) => { - const isPlugin = app.type === AppTypeEnum.plugin; - - const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(app.tmbId); - // Get app latest version - const { nodes, edges, chatConfig } = await getAppLatestVersion(app._id, app); - - const userQuestion: UserChatItemType = (() => { - if (isPlugin) { - return getPluginRunUserQuery({ - pluginInputs: getPluginInputsFromStoreNodes(nodes || app.modules), - variables - }); - } - - return { - obj: ChatRoleEnum.Human, - value: [ - { - type: ChatItemValueTypeEnum.text, - text: { - content: variables.question - } - } - ] - }; - })(); - - let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)); - if (isPlugin) { - // Assign values to runtimeNodes using variables - runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables); - // Plugin runtime does not need global variables(It has been injected into the pluginInputNode) - variables = {}; - } else { - delete variables.question; - variables.system_fileUrlList = variables.fileUrlList; - delete variables.fileUrlList; - } - - const chatId = getNanoid(); - - const { flowUsages, assistantResponses, newVariables, flowResponses, durationSeconds } = - await dispatchWorkFlow({ - chatId, - timezone, - externalProvider, - mode: 'chat', - runningAppInfo: { - id: String(app._id), - teamId: String(app.teamId), - tmbId: String(app.tmbId) - }, - runningUserInfo: { - teamId: String(app.teamId), - tmbId: String(app.tmbId) - }, - uid: String(app.tmbId), - runtimeNodes, - runtimeEdges: storeEdges2RuntimeEdges(edges), - variables, - query: removeEmptyUserInput(userQuestion.value), - chatConfig, - histories: [], - stream: false, - maxRunTimes: WORKFLOW_MAX_RUN_TIMES - }); - - // Save chat - const aiResponse: AIChatItemType & { dataId?: string } = { - obj: ChatRoleEnum.AI, - value: assistantResponses, - [DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses - }; - const newTitle = isPlugin ? 'Mcp call' : getChatTitleFromChatMessage(userQuestion); - await saveChat({ - chatId, - appId: app._id, - teamId: app.teamId, - tmbId: app.tmbId, - nodes, - appChatConfig: chatConfig, - variables: newVariables, - isUpdateUseTime: false, // owner update use time - newTitle, - source: ChatSourceEnum.mcp, - content: [userQuestion, aiResponse], - durationSeconds - }); - - // Push usage - createChatUsage({ - appName: app.name, - appId: app._id, - teamId: app.teamId, - tmbId: app.tmbId, - source: UsageSourceEnum.mcp, - flowUsages - }); - - // Get MCP response type - const responseContent = (() => { - if (isPlugin) { - const output = flowResponses.find( - (item) => item.moduleType === FlowNodeTypeEnum.pluginOutput - ); - if (output) { - return JSON.stringify(output.pluginOutput); - } else { - return 'Can not get response from plugin'; - } - } - - return assistantResponses - .map((item) => item?.text?.content) - .filter(Boolean) - .join('\n'); - })(); - - return responseContent; -}; - async function handler( req: ApiRequestProps, res: ApiResponseType ): Promise { - const { key, toolName, inputs } = req.body; - - const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean(); - - if (!mcp) { - return Promise.reject(CommonErrEnum.invalidResource); - } - - // Get app list - const appList = await MongoApp.find({ - _id: { $in: mcp.apps.map((app) => app.appId) }, - type: { $in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin] } - }).lean(); - - const app = appList.find((app) => { - const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!; - - return toolName === mcpApp.toolName; - }); - - if (!app) { - return Promise.reject(CommonErrEnum.missingParams); - } - - return await dispatchApp(app, inputs); + return callMcpServerTool(req.body); } export default NextAPI(handler); diff --git a/projects/app/src/pages/api/support/mcp/server/toolList.ts b/projects/app/src/pages/api/support/mcp/server/toolList.ts index 7eddb726a..9c5b7f5a1 100644 --- a/projects/app/src/pages/api/support/mcp/server/toolList.ts +++ b/projects/app/src/pages/api/support/mcp/server/toolList.ts @@ -1,17 +1,7 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; -import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; -import { MongoApp } from '@fastgpt/service/core/app/schema'; -import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth'; -import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; -import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; import { Tool } from '@modelcontextprotocol/sdk/types'; -import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; -import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; -import { AppChatConfigType } from '@fastgpt/global/core/app/type'; -import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; -import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; +import { getMcpServerTools } from '@/service/support/mcp/utils'; export type listToolsQuery = { key: string }; @@ -19,139 +9,13 @@ export type listToolsBody = {}; export type listToolsResponse = {}; -export const pluginNodes2InputSchema = ( - nodes: { flowNodeType: FlowNodeTypeEnum; inputs: FlowNodeInputItemType[] }[] -) => { - const pluginInput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput); - - const schema: Tool['inputSchema'] = { - type: 'object', - properties: {}, - required: [] - }; - - pluginInput?.inputs.forEach((input) => { - const jsonSchema = ( - toolValueTypeList.find((type) => type.value === input.valueType) || toolValueTypeList[0] - )?.jsonSchema; - - schema.properties![input.key] = { - ...jsonSchema, - description: input.description, - enum: input.enum?.split('\n').filter(Boolean) || undefined - }; - - if (input.required) { - // @ts-ignore - schema.required.push(input.key); - } - }); - - return schema; -}; -export const workflow2InputSchema = (chatConfig?: { - fileSelectConfig?: AppChatConfigType['fileSelectConfig']; - variables?: AppChatConfigType['variables']; -}) => { - const schema: Tool['inputSchema'] = { - type: 'object', - properties: { - question: { - type: 'string', - description: 'Question from user' - }, - ...(chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg - ? { - fileUrlList: { - type: 'array', - items: { - type: 'string' - }, - description: 'File linkage' - } - } - : {}) - }, - required: ['question'] - }; - - chatConfig?.variables?.forEach((item) => { - const jsonSchema = ( - toolValueTypeList.find((type) => type.value === item.valueType) || toolValueTypeList[0] - )?.jsonSchema; - - schema.properties![item.key] = { - ...jsonSchema, - description: item.description, - enum: item.enums?.map((enumItem) => enumItem.value) || undefined - }; - - if (item.required) { - // @ts-ignore - schema.required!.push(item.key); - } - }); - - return schema; -}; - async function handler( req: ApiRequestProps, res: ApiResponseType ): Promise { const { key } = req.query; - const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean(); - - if (!mcp) { - return Promise.reject(CommonErrEnum.invalidResource); - } - - // Get app list - const appList = await MongoApp.find( - { - _id: { $in: mcp.apps.map((app) => app.appId) }, - type: { $in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin] } - }, - { name: 1, intro: 1 } - ).lean(); - - // Filter not permission app - const permissionAppList = await Promise.all( - appList.filter(async (app) => { - try { - await authAppByTmbId({ tmbId: mcp.tmbId, appId: app._id, per: ReadPermissionVal }); - return true; - } catch (error) { - return false; - } - }) - ); - - // Get latest version - const versionList = await Promise.all( - permissionAppList.map((app) => getAppLatestVersion(app._id, app)) - ); - - // Compute mcp tools - const tools = versionList.map((version, index) => { - const app = permissionAppList[index]; - const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!; - - const isPlugin = !!version.nodes.find( - (node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput - ); - - return { - name: mcpApp.toolName, - description: mcpApp.description, - inputSchema: isPlugin - ? pluginNodes2InputSchema(version.nodes) - : workflow2InputSchema(version.chatConfig) - }; - }); - - return tools; + return getMcpServerTools(key); } export default NextAPI(handler); diff --git a/projects/app/src/service/support/mcp/type.d.ts b/projects/app/src/service/support/mcp/type.d.ts new file mode 100644 index 000000000..b65f91c4b --- /dev/null +++ b/projects/app/src/service/support/mcp/type.d.ts @@ -0,0 +1,5 @@ +export type toolCallProps = { + key: string; + toolName: string; + inputs: Record; +}; diff --git a/projects/app/src/service/support/mcp/utils.ts b/projects/app/src/service/support/mcp/utils.ts new file mode 100644 index 000000000..f535e1c44 --- /dev/null +++ b/projects/app/src/service/support/mcp/utils.ts @@ -0,0 +1,319 @@ +import { MongoMcpKey } from '@fastgpt/service/support/mcp/schema'; +import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { authAppByTmbId } from '@fastgpt/service/support/permission/app/auth'; +import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; +import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; +import { Tool } from '@modelcontextprotocol/sdk/types'; +import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { toolValueTypeList } from '@fastgpt/global/core/workflow/constants'; +import { AppChatConfigType } from '@fastgpt/global/core/app/type'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; +import { toolCallProps } from './type'; +import { AppSchema } from '@fastgpt/global/core/app/type'; +import { getUserChatInfoAndAuthTeamPoints } from '@fastgpt/service/support/permission/auth/team'; +import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type'; +import { + getPluginRunUserQuery, + updatePluginInputByVariables +} from '@fastgpt/global/core/workflow/utils'; +import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils'; +import { + ChatItemValueTypeEnum, + ChatRoleEnum, + ChatSourceEnum +} from '@fastgpt/global/core/chat/constants'; +import { + getWorkflowEntryNodeIds, + storeEdges2RuntimeEdges, + storeNodes2RuntimeNodes +} from '@fastgpt/global/core/workflow/runtime/utils'; +import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants'; +import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; +import { getChatTitleFromChatMessage, removeEmptyUserInput } from '@fastgpt/global/core/chat/utils'; +import { saveChat } from '@fastgpt/service/core/chat/saveChat'; +import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants'; +import { createChatUsage } from '@fastgpt/service/support/wallet/usage/controller'; +import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants'; + +export const pluginNodes2InputSchema = ( + nodes: { flowNodeType: FlowNodeTypeEnum; inputs: FlowNodeInputItemType[] }[] +) => { + const pluginInput = nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput); + + const schema: Tool['inputSchema'] = { + type: 'object', + properties: {}, + required: [] + }; + + pluginInput?.inputs.forEach((input) => { + const jsonSchema = ( + toolValueTypeList.find((type) => type.value === input.valueType) || toolValueTypeList[0] + )?.jsonSchema; + + schema.properties![input.key] = { + ...jsonSchema, + description: input.description, + enum: input.enum?.split('\n').filter(Boolean) || undefined + }; + + if (input.required) { + // @ts-ignore + schema.required.push(input.key); + } + }); + + return schema; +}; +export const workflow2InputSchema = (chatConfig?: { + fileSelectConfig?: AppChatConfigType['fileSelectConfig']; + variables?: AppChatConfigType['variables']; +}) => { + const schema: Tool['inputSchema'] = { + type: 'object', + properties: { + question: { + type: 'string', + description: 'Question from user' + }, + ...(chatConfig?.fileSelectConfig?.canSelectFile || chatConfig?.fileSelectConfig?.canSelectImg + ? { + fileUrlList: { + type: 'array', + items: { + type: 'string' + }, + description: 'File linkage' + } + } + : {}) + }, + required: ['question'] + }; + + chatConfig?.variables?.forEach((item) => { + const jsonSchema = ( + toolValueTypeList.find((type) => type.value === item.valueType) || toolValueTypeList[0] + )?.jsonSchema; + + schema.properties![item.key] = { + ...jsonSchema, + description: item.description, + enum: item.enums?.map((enumItem) => enumItem.value) || undefined + }; + + if (item.required) { + // @ts-ignore + schema.required!.push(item.key); + } + }); + + return schema; +}; +export const getMcpServerTools = async (key: string): Promise => { + const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean(); + if (!mcp) { + return Promise.reject(CommonErrEnum.invalidResource); + } + + // Get app list + const appList = await MongoApp.find( + { + _id: { $in: mcp.apps.map((app) => app.appId) }, + type: { $in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin] } + }, + { name: 1, intro: 1 } + ).lean(); + + // Filter not permission app + const permissionAppList = await Promise.all( + appList.filter(async (app) => { + try { + await authAppByTmbId({ tmbId: mcp.tmbId, appId: app._id, per: ReadPermissionVal }); + return true; + } catch (error) { + return false; + } + }) + ); + + // Get latest version + const versionList = await Promise.all( + permissionAppList.map((app) => getAppLatestVersion(app._id, app)) + ); + + // Compute mcp tools + const tools = versionList.map((version, index) => { + const app = permissionAppList[index]; + const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!; + + const isPlugin = !!version.nodes.find( + (node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput + ); + + return { + name: mcpApp.toolName, + description: mcpApp.description, + inputSchema: isPlugin + ? pluginNodes2InputSchema(version.nodes) + : workflow2InputSchema(version.chatConfig) + }; + }); + + return tools; +}; + +// Call tool +export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps) => { + const dispatchApp = async (app: AppSchema, variables: Record) => { + const isPlugin = app.type === AppTypeEnum.plugin; + + const { timezone, externalProvider } = await getUserChatInfoAndAuthTeamPoints(app.tmbId); + // Get app latest version + const { nodes, edges, chatConfig } = await getAppLatestVersion(app._id, app); + + const userQuestion: UserChatItemType = (() => { + if (isPlugin) { + return getPluginRunUserQuery({ + pluginInputs: getPluginInputsFromStoreNodes(nodes || app.modules), + variables + }); + } + + return { + obj: ChatRoleEnum.Human, + value: [ + { + type: ChatItemValueTypeEnum.text, + text: { + content: variables.question + } + } + ] + }; + })(); + + let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)); + if (isPlugin) { + // Assign values to runtimeNodes using variables + runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables); + // Plugin runtime does not need global variables(It has been injected into the pluginInputNode) + variables = {}; + } else { + delete variables.question; + variables.system_fileUrlList = variables.fileUrlList; + delete variables.fileUrlList; + } + + const chatId = getNanoid(); + + const { flowUsages, assistantResponses, newVariables, flowResponses, durationSeconds } = + await dispatchWorkFlow({ + chatId, + timezone, + externalProvider, + mode: 'chat', + runningAppInfo: { + id: String(app._id), + teamId: String(app.teamId), + tmbId: String(app.tmbId) + }, + runningUserInfo: { + teamId: String(app.teamId), + tmbId: String(app.tmbId) + }, + uid: String(app.tmbId), + runtimeNodes, + runtimeEdges: storeEdges2RuntimeEdges(edges), + variables, + query: removeEmptyUserInput(userQuestion.value), + chatConfig, + histories: [], + stream: false, + maxRunTimes: WORKFLOW_MAX_RUN_TIMES + }); + + // Save chat + const aiResponse: AIChatItemType & { dataId?: string } = { + obj: ChatRoleEnum.AI, + value: assistantResponses, + [DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses + }; + const newTitle = isPlugin ? 'Mcp call' : getChatTitleFromChatMessage(userQuestion); + await saveChat({ + chatId, + appId: app._id, + teamId: app.teamId, + tmbId: app.tmbId, + nodes, + appChatConfig: chatConfig, + variables: newVariables, + isUpdateUseTime: false, // owner update use time + newTitle, + source: ChatSourceEnum.mcp, + content: [userQuestion, aiResponse], + durationSeconds + }); + + // Push usage + createChatUsage({ + appName: app.name, + appId: app._id, + teamId: app.teamId, + tmbId: app.tmbId, + source: UsageSourceEnum.mcp, + flowUsages + }); + + // Get MCP response type + let responseContent = (() => { + if (isPlugin) { + const output = flowResponses.find( + (item) => item.moduleType === FlowNodeTypeEnum.pluginOutput + ); + if (output) { + return JSON.stringify(output.pluginOutput); + } else { + return 'Can not get response from plugin'; + } + } + + return assistantResponses + .map((item) => item?.text?.content) + .filter(Boolean) + .join('\n'); + })(); + + // Format response content + responseContent = responseContent.trim().replace(/\[\w+\]\(QUOTE\)/g, ''); + + return responseContent; + }; + + const mcp = await MongoMcpKey.findOne({ key }, { apps: 1 }).lean(); + + if (!mcp) { + return Promise.reject(CommonErrEnum.invalidResource); + } + + // Get app list + const appList = await MongoApp.find({ + _id: { $in: mcp.apps.map((app) => app.appId) }, + type: { $in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin] } + }).lean(); + + const app = appList.find((app) => { + const mcpApp = mcp.apps.find((mcpApp) => String(mcpApp.appId) === String(app._id))!; + + return toolName === mcpApp.toolName; + }); + + if (!app) { + return Promise.reject(CommonErrEnum.missingParams); + } + + return await dispatchApp(app, inputs); +}; diff --git a/projects/mcp_server/package.json b/projects/mcp_server/package.json index 09ac42974..f0799527b 100644 --- a/projects/mcp_server/package.json +++ b/projects/mcp_server/package.json @@ -14,7 +14,7 @@ "mcp_test": "npx @modelcontextprotocol/inspector" }, "dependencies": { - "@modelcontextprotocol/sdk": "1.9.0", + "@modelcontextprotocol/sdk": "^1.10.2", "axios": "^1.8.2", "chalk": "^5.3.0", "dayjs": "^1.11.7", diff --git a/test/cases/pages/api/support/mcp/server/toolList.test.ts b/test/cases/pages/api/support/mcp/server/toolList.test.ts index 6491ebe71..404948586 100644 --- a/test/cases/pages/api/support/mcp/server/toolList.test.ts +++ b/test/cases/pages/api/support/mcp/server/toolList.test.ts @@ -1,8 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { - pluginNodes2InputSchema, - workflow2InputSchema -} from '@/pages/api/support/mcp/server/toolList'; +import { pluginNodes2InputSchema, workflow2InputSchema } from '@/service/support/mcp/utils'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { VariableInputEnum, @@ -22,7 +19,8 @@ vi.mock('@fastgpt/service/core/app/schema', () => ({ find: vi.fn().mockReturnValue({ lean: vi.fn() }) - } + }, + AppCollectionName: 'apps' })); vi.mock('@fastgpt/service/core/app/version/controller', () => ({