mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
feat: streamable http mcp (#4695)
* feat: streamable http mcp * mcp api path * fix: ts
This commit is contained in:
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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": "开始使用",
|
||||
|
@@ -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": "開始使用",
|
||||
|
96
pnpm-lock.yaml
generated
96
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
@@ -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",
|
||||
|
@@ -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]);
|
||||
|
||||
|
@@ -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<LinkWay>('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 (
|
||||
<MyModal isOpen title={t('dashboard_mcp:usage_way')} onClose={onClose}>
|
||||
<MyModal iconSrc="key" isOpen title={t('dashboard_mcp:usage_way')} onClose={onClose}>
|
||||
<ModalBody>
|
||||
<Box>
|
||||
<FormLabel>{t('dashboard_mcp:mcp_endpoints')}</FormLabel>
|
||||
<HStack mt={0.5} bg={'myGray.50'} px={2} py={1} borderRadius={'md'} fontSize={'sm'}>
|
||||
<Box userSelect={'all'} flex={'1 0 0'} whiteSpace={'pre-wrap'} wordBreak={'break-all'}>
|
||||
{sseUrl}
|
||||
<Flex>
|
||||
<LightRowTabs<LinkWay>
|
||||
m={'auto'}
|
||||
w={'100%'}
|
||||
list={[
|
||||
{ label: 'Streamable HTTP', value: 'http' },
|
||||
{ label: 'SSE', value: 'sse' }
|
||||
]}
|
||||
value={linkWay}
|
||||
onChange={setLinkWay}
|
||||
/>
|
||||
</Flex>
|
||||
{url ? (
|
||||
<>
|
||||
<Box mt={4}>
|
||||
<FormLabel>{t('dashboard_mcp:mcp_endpoints')}</FormLabel>
|
||||
<HStack mt={0.5} bg={'myGray.50'} px={2} py={1} borderRadius={'md'} fontSize={'sm'}>
|
||||
<Box
|
||||
userSelect={'all'}
|
||||
flex={'1 0 0'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
>
|
||||
{url}
|
||||
</Box>
|
||||
<CopyBox value={url}>
|
||||
<MyIconButton icon="copy" />
|
||||
</CopyBox>
|
||||
</HStack>
|
||||
</Box>
|
||||
<CopyBox value={sseUrl}>
|
||||
<MyIconButton icon="copy" />
|
||||
</CopyBox>
|
||||
</HStack>
|
||||
</Box>
|
||||
<Box mt={4}>
|
||||
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
|
||||
<Flex
|
||||
p={3}
|
||||
bg={'myWhite.500'}
|
||||
border={'base'}
|
||||
borderTopLeftRadius={'md'}
|
||||
borderTopRightRadius={'md'}
|
||||
>
|
||||
<Box flex={1}>{t('dashboard_mcp:mcp_json_config')}</Box>
|
||||
<CopyBox value={jsonConfig}>
|
||||
<MyIconButton icon="copy" />
|
||||
</CopyBox>
|
||||
</Flex>
|
||||
<Box whiteSpace={'pre-wrap'} wordBreak={'break-all'} p={3} overflowX={'auto'}>
|
||||
{jsonConfig}
|
||||
<Box mt={4}>
|
||||
<Box borderRadius={'md'} bg={'myGray.100'} overflow={'hidden'} fontSize={'sm'}>
|
||||
<Flex
|
||||
p={3}
|
||||
bg={'myWhite.500'}
|
||||
border={'base'}
|
||||
borderTopLeftRadius={'md'}
|
||||
borderTopRightRadius={'md'}
|
||||
>
|
||||
<Box flex={1}>{t('dashboard_mcp:mcp_json_config')}</Box>
|
||||
<CopyBox value={jsonConfig}>
|
||||
<MyIconButton icon="copy" />
|
||||
</CopyBox>
|
||||
</Flex>
|
||||
<Box whiteSpace={'pre-wrap'} wordBreak={'break-all'} p={3} overflowX={'auto'}>
|
||||
{jsonConfig}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Flex h={'200px'} justifyContent={'center'} alignItems={'center'}>
|
||||
{t('dashboard_mcp:not_sse_server')}
|
||||
</Flex>
|
||||
)}
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
|
115
projects/app/src/pages/api/mcp/app/[key]/mcp.ts
Normal file
115
projects/app/src/pages/api/mcp/app/[key]/mcp.ts
Normal file
@@ -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<mcpBody, mcpQuery>, res: ApiResponseType<any>) => {
|
||||
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<string, any>
|
||||
): Promise<CallToolResult> => {
|
||||
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<mcpBody, mcpQuery>, res: ApiResponseType<any>) {
|
||||
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;
|
@@ -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<string, any>;
|
||||
};
|
||||
export type toolCallBody = toolCallProps;
|
||||
|
||||
export type toolCallResponse = {};
|
||||
|
||||
const dispatchApp = async (app: AppSchema, variables: Record<string, any>) => {
|
||||
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<toolCallBody, toolCallQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<toolCallResponse> {
|
||||
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);
|
||||
|
@@ -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<listToolsBody, listToolsQuery>,
|
||||
res: ApiResponseType<any>
|
||||
): Promise<Tool[]> {
|
||||
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<Tool>((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);
|
||||
|
5
projects/app/src/service/support/mcp/type.d.ts
vendored
Normal file
5
projects/app/src/service/support/mcp/type.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export type toolCallProps = {
|
||||
key: string;
|
||||
toolName: string;
|
||||
inputs: Record<string, any>;
|
||||
};
|
319
projects/app/src/service/support/mcp/utils.ts
Normal file
319
projects/app/src/service/support/mcp/utils.ts
Normal file
@@ -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<Tool[]> => {
|
||||
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<Tool>((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<string, any>) => {
|
||||
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);
|
||||
};
|
@@ -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",
|
||||
|
@@ -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', () => ({
|
||||
|
Reference in New Issue
Block a user