From 2302f39d73bb59591f944522063677213ca440ac Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Thu, 10 Apr 2025 09:20:28 +0800 Subject: [PATCH 1/6] feature/1.7-MCP --- frontend/package.json | 3 +- .../src/components/aoplatform/InsidePage.tsx | 4 +- .../src/contexts/GlobalStateContext.tsx | 14 + .../packages/common/src/hooks/pluginLoader.ts | 20 ++ .../common/src/locales/scan/en-US.json | 4 +- .../common/src/locales/scan/ja-JP.json | 4 +- .../common/src/locales/scan/zh-CN.json | 4 +- .../common/src/locales/scan/zh-TW.json | 4 +- .../core/src/const/ai-service/type.ts | 1 + frontend/packages/core/src/const/const.tsx | 16 ++ .../packages/core/src/const/system/const.tsx | 4 + .../packages/core/src/const/system/type.ts | 1 + .../pages/aiService/AiServiceInsidePage.tsx | 1 + .../core/src/pages/mcpService/AddMcpKey.tsx | 101 +++++++ .../mcpService/IntegrationAIContainer.tsx | 260 ++++++++++++++++++ .../src/pages/mcpService/McpKeyContainer.tsx | 200 ++++++++++++++ .../pages/mcpService/McpServiceContainer.tsx | 29 ++ .../core/src/pages/system/SystemConfig.tsx | 45 ++- .../src/pages/system/SystemInsidePage.tsx | 1 + .../core/src/pages/system/SystemList.tsx | 16 +- 20 files changed, 714 insertions(+), 18 deletions(-) create mode 100644 frontend/packages/core/src/pages/mcpService/AddMcpKey.tsx create mode 100644 frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx create mode 100644 frontend/packages/core/src/pages/mcpService/McpKeyContainer.tsx create mode 100644 frontend/packages/core/src/pages/mcpService/McpServiceContainer.tsx diff --git a/frontend/package.json b/frontend/package.json index 5998344c..9b1c9a92 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,7 +47,8 @@ "swagger-ui-react": "^5.17.14", "tailwindcss": "^3.3.5", "uuid": "^9.0.1", - "vite-tsconfig-paths": "^4.3.2" + "vite-tsconfig-paths": "^4.3.2", + "react-json-view": "^1.21.3" }, "devDependencies": { "@ant-design/cssinjs": "^1.18.2", diff --git a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx index 484a0814..7d9e7697 100644 --- a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx +++ b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx @@ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom' class InsidePageProps { showBanner?: boolean = true pageTitle: string | React.ReactNode = '' - tagList?: Array<{ label: string | ReactNode }> = [] + tagList?: Array<{ label: string | ReactNode; className?: string; color?: string }> = [] children: React.ReactNode showBtn?: boolean = false btnTitle?: string = '' @@ -79,7 +79,7 @@ const InsidePage: FC = ({ tagList?.length > 0 && tagList?.map((tag) => { return ( - + {tag.label} ) diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx index d230cc6d..a56c9448 100644 --- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -196,6 +196,20 @@ const mockData = [ key: 'maintenanceCenter', path: '/datasourcing', children: [ + { + name: 'MCP 服务', + key: 'mcpService', + path: '/mcpService', + icon: 'ph:network-x', + access: '' + }, + { + name: 'MCP Key', + key: 'mcpKey', + path: '/mcpKey', + icon: 'material-symbols:key', + access: '' + }, { name: '数据源', key: 'datasourcing', diff --git a/frontend/packages/common/src/hooks/pluginLoader.ts b/frontend/packages/common/src/hooks/pluginLoader.ts index 84f06bc4..f9ae66b1 100644 --- a/frontend/packages/common/src/hooks/pluginLoader.ts +++ b/frontend/packages/common/src/hooks/pluginLoader.ts @@ -220,6 +220,26 @@ const mockData = { } ] }, + { + driver: 'apipark.builtIn.component', + name: 'mcpService', + router: [ + { + path: 'mcpService', + type: 'normal' + } + ] + }, + { + driver: 'apipark.builtIn.component', + name: 'mcpKey', + router: [ + { + path: 'mcpKey', + type: 'normal' + } + ] + }, { driver: 'apipark.builtIn.component', name: 'loadBalancing', diff --git a/frontend/packages/common/src/locales/scan/en-US.json b/frontend/packages/common/src/locales/scan/en-US.json index 57f0ba51..23047b4d 100644 --- a/frontend/packages/common/src/locales/scan/en-US.json +++ b/frontend/packages/common/src/locales/scan/en-US.json @@ -898,5 +898,7 @@ "Kce2fcdbf": "No Permission", "K24f6a5b4": "Custom (Empty Template)", "Kea608112": "Load Preset Template", - "Kee7de862": "Edit Provider( (0) )" + "Kee7de862": "Edit Provider( (0) )", + "Kb0e0aeda": "New API Key", + "K9d81999c": "The API Key can be used to call system-level Open API and MCP." } diff --git a/frontend/packages/common/src/locales/scan/ja-JP.json b/frontend/packages/common/src/locales/scan/ja-JP.json index 61ea7256..694a8894 100644 --- a/frontend/packages/common/src/locales/scan/ja-JP.json +++ b/frontend/packages/common/src/locales/scan/ja-JP.json @@ -920,5 +920,7 @@ "Kce2fcdbf": "権限がありません", "K24f6a5b4": "カスタム(空のテンプレート)", "Kea608112": "プリセットテンプレートを読み込む", - "Kee7de862": "サプライヤーを編集( (0) )" + "Kee7de862": "サプライヤーを編集( (0) )", + "Kb0e0aeda": "APIキーを新規作成", + "K9d81999c": "APIキーは、システムレベルのOpen APIおよびMCPの呼び出しに使用できます。" } diff --git a/frontend/packages/common/src/locales/scan/zh-CN.json b/frontend/packages/common/src/locales/scan/zh-CN.json index f009105f..b8d834ae 100644 --- a/frontend/packages/common/src/locales/scan/zh-CN.json +++ b/frontend/packages/common/src/locales/scan/zh-CN.json @@ -851,5 +851,7 @@ "Kce2fcdbf": "暂无权限", "K24f6a5b4": "自定义(空模板)", "Kea608112": "载入预置模板", - "Kee7de862": "编辑供应商( (0) )" + "Kee7de862": "编辑供应商( (0) )", + "Kb0e0aeda": "新增 API Key", + "K9d81999c": "API 密钥可用于调用系统级 Open API 和 MCP。" } diff --git a/frontend/packages/common/src/locales/scan/zh-TW.json b/frontend/packages/common/src/locales/scan/zh-TW.json index fe2aae97..72e7bbfc 100644 --- a/frontend/packages/common/src/locales/scan/zh-TW.json +++ b/frontend/packages/common/src/locales/scan/zh-TW.json @@ -920,5 +920,7 @@ "Kce2fcdbf": "暫無權限", "K24f6a5b4": "自訂(空模板)", "Kea608112": "載入預設模板", - "Kee7de862": "編輯供應商( (0) )" + "Kee7de862": "編輯供應商( (0) )", + "Kb0e0aeda": "新增 API 金鑰", + "K9d81999c": "API 金鑰可用於調用系統級 Open API 和 MCP。" } diff --git a/frontend/packages/core/src/const/ai-service/type.ts b/frontend/packages/core/src/const/ai-service/type.ts index 0c3e2841..82c3825c 100644 --- a/frontend/packages/core/src/const/ai-service/type.ts +++ b/frontend/packages/core/src/const/ai-service/type.ts @@ -21,6 +21,7 @@ export type AiServiceConfigFieldType = { catalogue?:string | string[]; approvalType?:string; providerType?:string + enable_mcp?: boolean }; export type AiServiceSubServiceTableListItem = { diff --git a/frontend/packages/core/src/const/const.tsx b/frontend/packages/core/src/const/const.tsx index 1ee3f57b..f763da82 100644 --- a/frontend/packages/core/src/const/const.tsx +++ b/frontend/packages/core/src/const/const.tsx @@ -800,6 +800,22 @@ export const routerMap: Map = new Map([ ] } ], + [ + 'mcpService', + { + type: 'module', + lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpServiceContainer')), + key: 'mcpService' + } + ], + [ + 'mcpKey', + { + type: 'module', + lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpKeyContainer')), + key: 'mcpKey' + } + ], [ 'loadBalancing', { diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index bfd95cf1..b7dee443 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -362,6 +362,10 @@ export const SERVICE_APPROVAL_OPTIONS = [ { label: '无需审核:允许任何消费者调用该服务', value: 'auto' }, { label: '人工审核:仅允许通过人工审核的消费者调用该服务', value: 'manual' } ] +export const MCP_OPTIONS = [ + { label: '关闭', value: false }, + { label: '开启:AI Agent 等产品能够通过 MCP 方式调用服务', value: true } +] export const SERVICE_KIND_OPTIONS = [ { label: 'REST', value: 'rest' }, { label: 'AI', value: 'ai' } diff --git a/frontend/packages/core/src/const/system/type.ts b/frontend/packages/core/src/const/system/type.ts index c5f05336..9d4a82fe 100644 --- a/frontend/packages/core/src/const/system/type.ts +++ b/frontend/packages/core/src/const/system/type.ts @@ -31,6 +31,7 @@ export type SystemConfigFieldType = { catalogue?:string | string[]; approvalType?:string; modelMapping?: string; + enable_mcp?: boolean; }; export type SystemSubServiceTableListItem = { diff --git a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx index c438539f..025ac7fd 100644 --- a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx +++ b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx @@ -228,6 +228,7 @@ const AiServiceInsidePage: FC = () => { diff --git a/frontend/packages/core/src/pages/mcpService/AddMcpKey.tsx b/frontend/packages/core/src/pages/mcpService/AddMcpKey.tsx new file mode 100644 index 00000000..2c786c4a --- /dev/null +++ b/frontend/packages/core/src/pages/mcpService/AddMcpKey.tsx @@ -0,0 +1,101 @@ +import { App, Form, Input } from 'antd' +import { $t } from '@common/locales' +import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' +import { v4 as uuidv4 } from 'uuid' + +import { forwardRef, useEffect, useImperativeHandle } from 'react' +type modelFieldType = { + name: string + type: string + model_parameters: string + access_configuration: string +} + +export type addMcpKeysHandle = { + save: () => Promise +} + +type addMcpKeysProps = { + name?: string + value?: string + type?: string + apikey?: string +} + +const AddMcpKey = forwardRef((props, ref) => { + const { name = '', value: editValue = '', type = 'new', apikey = '' } = props + const [form] = Form.useForm() + const { message } = App.useApp() + const { fetchData } = useFetch() + + useEffect(() => { + form.setFieldsValue({ + name, + value: editValue + }) + }, []) + /** + * 保存 + * @returns + */ + const save: () => Promise = () => { + return new Promise((resolve, reject) => { + try { + form + .validateFields() + .then((value) => { + console.log('value', value) + const finalValue = { + ...value, + value: editValue ? editValue : uuidv4(), + expired: 0 + } + fetchData>('system/apikey', { + method: type === 'new' ? 'POST' : 'PUT', + eoBody: finalValue, + ...(type === 'edit' ? { + eoParams: { apikey } + } : {}) + }) + .then((response) => { + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + message.success($t(RESPONSE_TIPS.success) || msg) + resolve(true) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + reject(msg || $t(RESPONSE_TIPS.error)) + } + }) + .catch((errorInfo) => reject(errorInfo)) + }) + .catch((errorInfo) => reject(errorInfo)) + } catch (error) { + reject(error) + } + }) + } + + useImperativeHandle(ref, () => ({ + save + })) + + return ( +
+ label={$t('名称')} name="name" rules={[{ required: true }]}> + + + + ) +}) + +export default AddMcpKey diff --git a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx new file mode 100644 index 00000000..80b99fdc --- /dev/null +++ b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx @@ -0,0 +1,260 @@ +import { App, Card, Select } from 'antd' +import { $t } from '@common/locales/index.ts' +import { Icon } from '@iconify/react/dist/iconify.js' +import { useEffect, useState } from 'react' +import ReactJson from 'react-json-view' +import { IconButton } from '@common/components/postcat/api/IconButton' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' + +type ConfigList = { + openApi?: { + title: string + configContent: string + apiKeys: string[] + } + mcp: { + title: string + configContent: string + apiKeys: string[] + } +} + +type ApiKeyItem = { + expired: number + id: string + name: string + value: string +} + +const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | 'service'; handleApiKeyChange: (value: string) => void }) => { + const [activeTab, setActiveTab] = useState('mcp') + const { message } = App.useApp() + const [configContent, setConfigContent] = useState('') + const [apiKey, setApiKey] = useState('暂无数据') + const [apiKeyList, setApiKeyList] = useState<{ value: string; label: string }[]>([]) + const [tabContent, setTabContent] = useState({ + mcp: { + title: $t('MCP 配置'), + configContent: '', + apiKeys: [] + } + }) + const { fetchData } = useFetch() + + const initTabsData = () => { + const params: ConfigList = { + mcp: { + title: $t('MCP 配置'), + configContent: '', + apiKeys: [] + } + } + if (type === 'global') { + params.openApi = { + title: $t('Open API 文档'), + configContent: '', + apiKeys: [] + } + } + setTabContent(params) + } + + /** + * 复制 + * @param value + * @returns + */ + const handleCopy = async (value: string): Promise => { + if (value) { + await navigator.clipboard.writeText(value) + message.success($t(RESPONSE_TIPS.copySuccess)) + } + } + + const handleChange = (value: string) => { + setApiKey(value) + handleApiKeyChange(value) + } + + /** + * 获取全局 MCP 配置 + * @returns + */ + const getGlobalMcpConfig = () => { + fetchData>('global/mcp/config', { + method: 'GET' + }) + .then((response) => { + const { code, msg, data } = response + if (code === STATUS_CODE.SUCCESS) { + setTabContent((prevTabContent) => ({ + ...prevTabContent, + mcp: { + ...prevTabContent.mcp, + configContent: data.config || '' + } + })) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + .catch((errorInfo) => { + message.error(errorInfo || $t(RESPONSE_TIPS.error)) + }) + } + + /** + * 获取 API Key 列表 + */ + const getKeysList = () => { + fetchData>(type === 'global' ? 'simple/system/apikeys' : '', { + method: 'GET' + }) + .then((response) => { + const { code, msg, data } = response + if (code === STATUS_CODE.SUCCESS) { + if (data.apikeys && data.apikeys.length > 0) { + setApiKeyList( + data.apikeys.map((item: ApiKeyItem) => { + return { + label: item.name, + value: item.value + } + }) + ) + setApiKey(data.apikeys[0].value) + } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + .catch((errorInfo) => { + message.error(errorInfo || $t(RESPONSE_TIPS.error)) + }) + } + + useEffect(() => { + type === 'global' && getGlobalMcpConfig() + initTabsData() + getKeysList() + }, []) + useEffect(() => { + if (activeTab === 'openApi') { + setConfigContent(tabContent.openApi?.configContent || '') + } else if (activeTab === 'mcp') { + setConfigContent(tabContent.mcp.configContent || '') + } + }, [tabContent, activeTab]) + return ( + <> + +

+ + {$t('AI 代理集成')} +

+
+ {type === 'service' && ( +
+
setActiveTab('openApi')} + > + Open API +
+
setActiveTab('mcp')} + > + MCP +
+
+ )} +
+ {activeTab === 'openApi' ? tabContent.openApi?.title : tabContent.mcp.title} +
+ {/* 标签页内容区域 */} +
+ + handleCopy(configContent)} + sx={{ + position: 'absolute', + top: '5px', + right: '5px', + color: '#999', + transition: 'none', + '&.MuiButtonBase-root:hover': { + background: 'transparent', + color: '#3D46F2', + transition: 'none' + } + }} + > +
+
+ {activeTab === 'mcp' && ( + <> +
API Key
+ diff --git a/frontend/packages/core/src/pages/system/SystemInsidePage.tsx b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx index ef1fee32..d7a174dc 100644 --- a/frontend/packages/core/src/pages/system/SystemInsidePage.tsx +++ b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx @@ -234,6 +234,7 @@ const SystemInsidePage: FC = () => { diff --git a/frontend/packages/core/src/pages/system/SystemList.tsx b/frontend/packages/core/src/pages/system/SystemList.tsx index 4e0f5f12..06b53ab5 100644 --- a/frontend/packages/core/src/pages/system/SystemList.tsx +++ b/frontend/packages/core/src/pages/system/SystemList.tsx @@ -8,7 +8,7 @@ import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx' import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx' import { useFetch } from '@common/hooks/http.ts' import { $t } from '@common/locales/index.ts' -import { App } from 'antd' +import { App, Tag } from 'antd' import { FC, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/const.tsx' @@ -176,10 +176,16 @@ const SystemList: FC = () => { x.valueEnum = teamList } if ((x.dataIndex as string) === 'service_kind') { - x.valueEnum = {} - SERVICE_KIND_OPTIONS.forEach((option) => { - ;(x.valueEnum as any)[option.value] = { text: $t(option.label) } - }) + x.render = (dom: React.ReactNode, record: any) => ( + + {$t(SERVICE_KIND_OPTIONS.find((x) => x.value === record.service_kind)?.label || '-')} + {record.enable_mcp && ( + MCP + )} + + ) } if ((x.dataIndex as string) === 'state') { x.render = (dom: React.ReactNode, record: any) => ( From 8cff5b3d40f191a208b8b8ce28db6c9ce521a4c1 Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Thu, 10 Apr 2025 09:48:24 +0800 Subject: [PATCH 2/6] feature/1.7-MCP --- frontend/packages/core/src/pages/system/SystemConfig.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/packages/core/src/pages/system/SystemConfig.tsx b/frontend/packages/core/src/pages/system/SystemConfig.tsx index c819041d..e36ec29e 100644 --- a/frontend/packages/core/src/pages/system/SystemConfig.tsx +++ b/frontend/packages/core/src/pages/system/SystemConfig.tsx @@ -23,6 +23,7 @@ import { v4 as uuidv4 } from 'uuid' import { SystemConfigFieldType, SystemConfigHandle } from '../../const/system/type.ts' import { useSystemContext } from '../../contexts/SystemContext.tsx' import { Codebox } from '@common/components/postcat/api/Codebox/index.tsx' +import { useAiServiceContext } from '@core/contexts/AiServiceContext.tsx' export type SimpleAiProviderItem = EntityItem & { configured: boolean @@ -39,6 +40,7 @@ const SystemConfig = forwardRef((_, ref) => { const navigate = useNavigate() const { setBreadcrumb } = useBreadcrumb() const { setSystemInfo } = useSystemContext() + const { setAiServiceInfo } = useAiServiceContext() const [showClassify, setShowClassify] = useState(true) const [showAI, setShowAI] = useState(false) const [imageBase64, setImageBase64] = useState(null) @@ -274,6 +276,7 @@ const SystemConfig = forwardRef((_, ref) => { if (code === STATUS_CODE.SUCCESS) { message.success(msg || $t(RESPONSE_TIPS.success)) setSystemInfo(data.service) + setAiServiceInfo(data.service) return Promise.resolve(true) } else { message.error(msg || $t(RESPONSE_TIPS.error)) From 1eeba2d6488f514d574cdca9263e45202dd3b8dd Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Thu, 10 Apr 2025 10:24:29 +0800 Subject: [PATCH 3/6] feature/1.7-MCP --- frontend/package.json | 4 +- .../mcpService/IntegrationAIContainer.tsx | 58 ++- .../src/pages/mcpService/hook/constants.ts | 33 ++ .../mcpService/hook/notificationTypes.ts | 22 + .../pages/mcpService/hook/useConnection.ts | 382 ++++++++++++++++++ 5 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 frontend/packages/core/src/pages/mcpService/hook/constants.ts create mode 100644 frontend/packages/core/src/pages/mcpService/hook/notificationTypes.ts create mode 100644 frontend/packages/core/src/pages/mcpService/hook/useConnection.ts diff --git a/frontend/package.json b/frontend/package.json index 9b1c9a92..0f6fe4b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,7 +48,9 @@ "tailwindcss": "^3.3.5", "uuid": "^9.0.1", "vite-tsconfig-paths": "^4.3.2", - "react-json-view": "^1.21.3" + "react-json-view": "^1.21.3", + "zod": "^3.23.8", + "@modelcontextprotocol/sdk": "^1.9.0" }, "devDependencies": { "@ant-design/cssinjs": "^1.18.2", diff --git a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx index 80b99fdc..108bd29e 100644 --- a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx +++ b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx @@ -6,6 +6,7 @@ import ReactJson from 'react-json-view' import { IconButton } from '@common/components/postcat/api/IconButton' import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' +import { useConnection } from './hook/useConnection' type ConfigList = { openApi?: { @@ -31,8 +32,9 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | const [activeTab, setActiveTab] = useState('mcp') const { message } = App.useApp() const [configContent, setConfigContent] = useState('') - const [apiKey, setApiKey] = useState('暂无数据') + const [apiKey, setApiKey] = useState('') const [apiKeyList, setApiKeyList] = useState<{ value: string; label: string }[]>([]) + const [mcpServerUrl, setMcpServerUrl] = useState('') const [tabContent, setTabContent] = useState({ mcp: { title: $t('MCP 配置'), @@ -134,6 +136,37 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | }) } + const { + connectionStatus, + serverCapabilities, + mcpClient, + requestHistory, + makeRequest: makeConnectionRequest, + sendNotification, + handleCompletion, + completionsSupported, + connect: connectMcpServer, + disconnect: disconnectMcpServer, + } = useConnection({ + transportType: 'sse', + sseUrl: mcpServerUrl, + proxyServerUrl: 'mcp/global/sse', + requestTimeout: 1000, + }); + console.log('connectionStatus==================', connectionStatus); + // console.log('serverCapabilities==================', serverCapabilities); + // console.log('mcpClient==================', mcpClient); + // console.log('requestHistory==================', requestHistory); + // console.log('makeConnectionRequest==================', makeConnectionRequest); + // console.log('sendNotification==================', sendNotification); + // console.log('handleCompletion==================', handleCompletion); + // console.log('completionsSupported==================', completionsSupported); + // console.log('connectMcpServer==================', connectMcpServer); + // console.log('disconnectMcpServer==================', disconnectMcpServer); + // const useConnectAIagent = () => { + // connectMcpServer() + // } + useEffect(() => { type === 'global' && getGlobalMcpConfig() initTabsData() @@ -146,6 +179,29 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | setConfigContent(tabContent.mcp.configContent || '') } }, [tabContent, activeTab]) + useEffect(() => { + if (configContent && apiKey) { + const parsedConfig = JSON.parse(configContent) + console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊parsedConfig', parsedConfig, apiKey) + let baseUrl = '' + if (parsedConfig?.mcpServers) { + // 获取 mcpServers 对象中的第一个键 + const serverKey = Object.keys(parsedConfig.mcpServers)[0] + baseUrl = parsedConfig.mcpServers[serverKey]?.url + } + baseUrl = baseUrl.replace('{your_api_key}', apiKey) + if (mcpServerUrl === baseUrl) { + return + } + setMcpServerUrl(baseUrl) + console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊', mcpServerUrl) + if (connectionStatus === 'connected') { + disconnectMcpServer() + } + connectMcpServer() + } + }, [apiKey, configContent, connectMcpServer]) + return ( <> ; +export type Notification = z.infer; diff --git a/frontend/packages/core/src/pages/mcpService/hook/useConnection.ts b/frontend/packages/core/src/pages/mcpService/hook/useConnection.ts new file mode 100644 index 00000000..4b82432e --- /dev/null +++ b/frontend/packages/core/src/pages/mcpService/hook/useConnection.ts @@ -0,0 +1,382 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { + SSEClientTransport, + SseError, +} from "@modelcontextprotocol/sdk/client/sse.js"; +import { App } from 'antd' +import { + ClientNotification, + ClientRequest, + CreateMessageRequestSchema, + ListRootsRequestSchema, + ProgressNotificationSchema, + ResourceUpdatedNotificationSchema, + LoggingMessageNotificationSchema, + Request, + Result, + ServerCapabilities, + PromptReference, + ResourceReference, + McpError, + CompleteResultSchema, + ErrorCode, + CancelledNotificationSchema, + ResourceListChangedNotificationSchema, + ToolListChangedNotificationSchema, + PromptListChangedNotificationSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { useState } from "react"; +import { z } from "zod"; +import { ConnectionStatus, SESSION_KEYS } from "./constants"; +import { Notification, StdErrNotificationSchema } from "./notificationTypes"; +// import { auth } from "@modelcontextprotocol/sdk/client/auth.js"; +// import { authProvider } from "../auth"; +// import packageJson from "../../../package.json"; + + +interface UseConnectionOptions { + transportType: "stdio" | "sse"; + command?: string; + args?: string; + sseUrl: string; + env?: Record; + proxyServerUrl: string; + bearerToken?: string; + requestTimeout?: number; + onNotification?: (notification: Notification) => void; + onStdErrNotification?: (notification: Notification) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onPendingRequest?: (request: any, resolve: any, reject: any) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getRoots?: () => any[]; +} + +interface RequestOptions { + signal?: AbortSignal; + timeout?: number; + suppressToast?: boolean; +} + +export function useConnection({ + transportType, + command, + args, + sseUrl, + env, + proxyServerUrl, + bearerToken, + requestTimeout, + onNotification, + onStdErrNotification, + onPendingRequest, + getRoots, +}: UseConnectionOptions) { + const [connectionStatus, setConnectionStatus] = + useState("disconnected"); + const { message } = App.useApp() + const [serverCapabilities, setServerCapabilities] = + useState(null); + const [mcpClient, setMcpClient] = useState(null); + const [requestHistory, setRequestHistory] = useState< + { request: string; response?: string }[] + >([]); + const [completionsSupported, setCompletionsSupported] = useState(true); + + const pushHistory = (request: object, response?: object) => { + setRequestHistory((prev) => [ + ...prev, + { + request: JSON.stringify(request), + response: response !== undefined ? JSON.stringify(response) : undefined, + }, + ]); + }; + + const makeRequest = async ( + request: ClientRequest, + schema: T, + options?: RequestOptions, + ): Promise> => { + if (!mcpClient) { + throw new Error("MCP client not connected"); + } + + try { + const abortController = new AbortController(); + const timeoutId = setTimeout(() => { + abortController.abort("Request timed out"); + }, options?.timeout ?? requestTimeout); + + let response; + try { + response = await mcpClient.request(request, schema, { + signal: options?.signal ?? abortController.signal, + }); + pushHistory(request, response); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + pushHistory(request, { error: errorMessage }); + throw error; + } finally { + clearTimeout(timeoutId); + } + + return response; + } catch (e: unknown) { + if (!options?.suppressToast) { + const errorString = (e as Error).message ?? String(e); + message.error(errorString) + } + throw e; + } + }; + + const handleCompletion = async ( + ref: ResourceReference | PromptReference, + argName: string, + value: string, + signal?: AbortSignal, + ): Promise => { + if (!mcpClient || !completionsSupported) { + return []; + } + + const request: ClientRequest = { + method: "completion/complete", + params: { + argument: { + name: argName, + value, + }, + ref, + }, + }; + + try { + const response = await makeRequest(request, CompleteResultSchema, { + signal, + suppressToast: true, + }); + return response?.completion.values || []; + } catch (e: unknown) { + // Disable completions silently if the server doesn't support them. + // See https://github.com/modelcontextprotocol/specification/discussions/122 + if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) { + setCompletionsSupported(false); + return []; + } + + // Unexpected errors - show toast and rethrow + message.error(e instanceof Error ? e.message : String(e)) + throw e; + } + }; + + const sendNotification = async (notification: ClientNotification) => { + if (!mcpClient) { + const error = new Error("MCP client not connected"); + message.error(error.message) + throw error; + } + + try { + await mcpClient.notification(notification); + // Log successful notifications + pushHistory(notification); + } catch (e: unknown) { + if (e instanceof McpError) { + // Log MCP protocol errors + pushHistory(notification, { error: e.message }); + } + message.error(e instanceof Error ? e.message : String(e)) + throw e; + } + }; + // TODO_先屏蔽,暂时不需要 + // const checkProxyHealth = async () => { + // try { + // const proxyHealthUrl = new URL(`${proxyServerUrl}/health`); + // const proxyHealthResponse = await fetch(proxyHealthUrl); + // const proxyHealth = await proxyHealthResponse.json(); + // if (proxyHealth?.status !== "ok") { + // throw new Error("MCP Proxy Server is not healthy"); + // } + // } catch (e) { + // console.error("Couldn't connect to MCP Proxy Server", e); + // throw e; + // } + // }; +// TODO_先屏蔽,暂时不需要 +// const handleAuthError = async (error: unknown) => { +// if (error instanceof SseError && error.code === 401) { +// sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl); + +// const result = await auth(authProvider, { serverUrl: sseUrl }); +// return result === "AUTHORIZED"; +// } + +// return false; +// }; + + const connect = async (_e?: unknown, retryCount: number = 0) => { + const client = new Client( + { + name: "mcp-inspector", + version: '0.0.1', + }, + { + capabilities: { + sampling: {}, + roots: { + listChanged: true, + }, + }, + }, + ); + // TODO_暂时不需要 + // try { + // await checkProxyHealth(); + // } catch { + // setConnectionStatus("error-connecting-to-proxy"); + // return; + // } + // 使用与http.ts一致的方式处理URL + // 注意:proxyServerUrl应该是完整URL,或者我们需要为其添加基础URL + // 处理两种情况:完整URL或相对路径 + let fullUrl; + if (proxyServerUrl.startsWith('http://') || proxyServerUrl.startsWith('https://')) { + // 如果是完整URL,直接使用 + fullUrl = `${proxyServerUrl}/sse`; + } else { + // 如果是相对路径,添加基础URL和API前缀 + const baseUrl = window.location.origin; + const apiPrefix = '/api/v1/'; + fullUrl = `${baseUrl}${apiPrefix}${proxyServerUrl}`; + } + const mcpProxyServerUrl = new URL(fullUrl); + mcpProxyServerUrl.searchParams.append("transportType", transportType); + if (transportType === "stdio") { + mcpProxyServerUrl.searchParams.append("command", command || ''); + mcpProxyServerUrl.searchParams.append("args", args || ''); + mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env || {})); + } else { + mcpProxyServerUrl.searchParams.append("url", sseUrl); + } + console.log('sseUrl===', sseUrl) + try { + // Inject auth manually instead of using SSEClientTransport, because we're + // proxying through the inspector server first. + const headers: HeadersInit = {}; + + // TODO_暂时不需要。Use manually provided bearer token if available, otherwise use OAuth tokens + // const token = bearerToken || (await authProvider.tokens())?.access_token; + // if (token) { + // headers["Authorization"] = `Bearer ${token}`; + // } + + // 创建SSE客户端传输层 + const clientTransport = new SSEClientTransport(mcpProxyServerUrl, { + eventSourceInit: { + fetch: (url, init) => fetch(url, { ...init, headers }), + }, + requestInit: { + headers, + }, + }); + // TODO_暂时不需要 + // if (onNotification) { + // [ + // CancelledNotificationSchema, + // ProgressNotificationSchema, + // LoggingMessageNotificationSchema, + // ResourceUpdatedNotificationSchema, + // ResourceListChangedNotificationSchema, + // ToolListChangedNotificationSchema, + // PromptListChangedNotificationSchema, + // ].forEach((notificationSchema) => { + // client.setNotificationHandler(notificationSchema, onNotification); + // }); + + // client.fallbackNotificationHandler = ( + // notification: Notification, + // ): Promise => { + // onNotification(notification); + // return Promise.resolve(); + // }; + // } + + // if (onStdErrNotification) { + // client.setNotificationHandler( + // StdErrNotificationSchema, + // onStdErrNotification, + // ); + // } + + try { + await client.connect(clientTransport); + } catch (error) { + console.error( + `Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`, + error, + ); + // TODO_先屏蔽,后续如果需要再处理 + // const shouldRetry = await handleAuthError(error); + // if (shouldRetry) { + // return connect(undefined, retryCount + 1); + // } + + if (error instanceof SseError && error.code === 401) { + // Don't set error state if we're about to redirect for auth + return; + } + throw error; + } + + const capabilities = client.getServerCapabilities(); + setServerCapabilities(capabilities ?? null); + setCompletionsSupported(true); // Reset completions support on new connection + // TODO_暂时不需要 + // if (onPendingRequest) { + // client.setRequestHandler(CreateMessageRequestSchema, (request) => { + // return new Promise((resolve, reject) => { + // onPendingRequest(request, resolve, reject); + // }); + // }); + // } + + // if (getRoots) { + // client.setRequestHandler(ListRootsRequestSchema, async () => { + // return { roots: getRoots() }; + // }); + // } + + setMcpClient(client); + setConnectionStatus("connected"); + } catch (e) { + console.error(e); + setConnectionStatus("error"); + } + }; + + const disconnect = async () => { + await mcpClient?.close(); + setMcpClient(null); + setConnectionStatus("disconnected"); + setCompletionsSupported(false); + setServerCapabilities(null); + }; + + return { + connectionStatus, + serverCapabilities, + mcpClient, + requestHistory, + makeRequest, + sendNotification, + handleCompletion, + completionsSupported, + connect, + disconnect, + }; +} From b0d642f5e3ce0c6a290d52a6e659c8473a2faccd Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Thu, 10 Apr 2025 15:26:28 +0800 Subject: [PATCH 4/6] feature/1.7-MCP --- .../mcpService/IntegrationAIContainer.tsx | 112 +++++++++++------- .../src/pages/mcpService/McpKeyContainer.tsx | 5 +- .../pages/mcpService/McpServiceContainer.tsx | 16 +-- .../pages/mcpService/McpToolsContainer.tsx | 30 +++++ .../pages/mcpService/hook/useConnection.ts | 28 +++-- 5 files changed, 131 insertions(+), 60 deletions(-) create mode 100644 frontend/packages/core/src/pages/mcpService/McpToolsContainer.tsx diff --git a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx index 108bd29e..248da0ee 100644 --- a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx +++ b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx @@ -7,6 +7,8 @@ import { IconButton } from '@common/components/postcat/api/IconButton' import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' import { useConnection } from './hook/useConnection' +import { ClientRequest, Tool, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' +import { z } from "zod"; type ConfigList = { openApi?: { @@ -28,13 +30,19 @@ type ApiKeyItem = { value: string } -const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | 'service'; handleApiKeyChange: (value: string) => void }) => { +const IntegrationAIContainer = ({ type, handleToolsChange }: { type: 'global' | 'service'; handleToolsChange: (value: Tool[]) => void }) => { const [activeTab, setActiveTab] = useState('mcp') const { message } = App.useApp() const [configContent, setConfigContent] = useState('') const [apiKey, setApiKey] = useState('') const [apiKeyList, setApiKeyList] = useState<{ value: string; label: string }[]>([]) const [mcpServerUrl, setMcpServerUrl] = useState('') + const [errors, setErrors] = useState>({ + resources: null, + prompts: null, + tools: null, + }); + const [tabContent, setTabContent] = useState({ mcp: { title: $t('MCP 配置'), @@ -76,7 +84,6 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | const handleChange = (value: string) => { setApiKey(value) - handleApiKeyChange(value) } /** @@ -136,6 +143,45 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | }) } + const clearError = (tabKey: keyof typeof errors) => { + setErrors((prev) => ({ ...prev, [tabKey]: null })); + }; + + const makeRequest = async ( + request: ClientRequest, + schema: T, + tabKey?: keyof typeof errors, + ) => { + try { + const response = await makeConnectionRequest(request, schema); + if (tabKey !== undefined) { + clearError(tabKey); + } + return response; + } catch (e) { + const errorString = (e as Error).message ?? String(e); + if (tabKey !== undefined) { + setErrors((prev) => ({ + ...prev, + [tabKey]: errorString, + })); + } + throw e; + } + }; + + const listTools = async () => { + const response = await makeRequest( + { + method: "tools/list" as const, + params: {}, + }, + ListToolsResultSchema, + "tools", + ); + handleToolsChange(response.tools) + }; + const { connectionStatus, serverCapabilities, @@ -149,69 +195,53 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | disconnect: disconnectMcpServer, } = useConnection({ transportType: 'sse', - sseUrl: mcpServerUrl, - proxyServerUrl: 'mcp/global/sse', + sseUrl: '', + proxyServerUrl: mcpServerUrl, requestTimeout: 1000, }); - console.log('connectionStatus==================', connectionStatus); - // console.log('serverCapabilities==================', serverCapabilities); - // console.log('mcpClient==================', mcpClient); - // console.log('requestHistory==================', requestHistory); - // console.log('makeConnectionRequest==================', makeConnectionRequest); - // console.log('sendNotification==================', sendNotification); - // console.log('handleCompletion==================', handleCompletion); - // console.log('completionsSupported==================', completionsSupported); - // console.log('connectMcpServer==================', connectMcpServer); - // console.log('disconnectMcpServer==================', disconnectMcpServer); - // const useConnectAIagent = () => { - // connectMcpServer() - // } useEffect(() => { - type === 'global' && getGlobalMcpConfig() + if (type === 'global') { + getGlobalMcpConfig() + setMcpServerUrl('mcp/global/sse') + } initTabsData() getKeysList() }, []) useEffect(() => { - if (activeTab === 'openApi') { - setConfigContent(tabContent.openApi?.configContent || '') - } else if (activeTab === 'mcp') { - setConfigContent(tabContent.mcp.configContent || '') + if (apiKey) { + if (activeTab === 'openApi' && tabContent?.openApi?.configContent) { + setConfigContent(tabContent?.openApi?.configContent?.replace('{your_api_key}', apiKey) || '') + } else if (activeTab === 'mcp' && tabContent?.mcp?.configContent) { + setConfigContent(tabContent.mcp.configContent?.replace('{your_api_key}', apiKey) || '') + } } - }, [tabContent, activeTab]) + }, [apiKey, activeTab, tabContent]) + useEffect(() => { - if (configContent && apiKey) { - const parsedConfig = JSON.parse(configContent) - console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊parsedConfig', parsedConfig, apiKey) - let baseUrl = '' - if (parsedConfig?.mcpServers) { - // 获取 mcpServers 对象中的第一个键 - const serverKey = Object.keys(parsedConfig.mcpServers)[0] - baseUrl = parsedConfig.mcpServers[serverKey]?.url - } - baseUrl = baseUrl.replace('{your_api_key}', apiKey) - if (mcpServerUrl === baseUrl) { - return - } - setMcpServerUrl(baseUrl) - console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊', mcpServerUrl) + if (mcpServerUrl) { if (connectionStatus === 'connected') { disconnectMcpServer() } connectMcpServer() } - }, [apiKey, configContent, connectMcpServer]) + }, [mcpServerUrl]) + useEffect(() => { + if (connectionStatus === 'connected') { + listTools() + } + }, [connectionStatus]) return ( <> -

+

{ * 编辑 API Key */ const editKey = (key: any) => { - console.log('any', key) modal.confirm({ title: $t('编辑'), content: ( @@ -145,7 +144,7 @@ const McpKeyContainer = () => {

{key.name}

-

+

{key.value} { } }} > -

+
{ - const handleApiKeyChange = (value: string) => { - console.log(value) + const [tools, setTools] = useState([]); + const handleToolsChange = (value: Tool[]) => { + setTools(value) } return ( <> @@ -15,11 +18,10 @@ const McpServiceContainer = () => { showBorder={false} scrollPage={false} > +
- - 444 - - + +
diff --git a/frontend/packages/core/src/pages/mcpService/McpToolsContainer.tsx b/frontend/packages/core/src/pages/mcpService/McpToolsContainer.tsx new file mode 100644 index 00000000..b3a98fc8 --- /dev/null +++ b/frontend/packages/core/src/pages/mcpService/McpToolsContainer.tsx @@ -0,0 +1,30 @@ +import { Icon } from '@iconify/react/dist/iconify.js' +import { Tool } from '@modelcontextprotocol/sdk/types.js' +import { Card } from 'antd' + +const McpToolsContainer = ({ tools = [] }: { tools: Tool[] }) => { + return ( + <> + +
+ + Tools +
+ {tools.map((tool, index) => ( + 0 ? 'mt-[10px]' : ''}`}> +

{tool.name}

+
{tool.description}
+
+ ))} +
+ + ) +} + +export default McpToolsContainer diff --git a/frontend/packages/core/src/pages/mcpService/hook/useConnection.ts b/frontend/packages/core/src/pages/mcpService/hook/useConnection.ts index 4b82432e..55365aa6 100644 --- a/frontend/packages/core/src/pages/mcpService/hook/useConnection.ts +++ b/frontend/packages/core/src/pages/mcpService/hook/useConnection.ts @@ -254,16 +254,26 @@ export function useConnection({ const apiPrefix = '/api/v1/'; fullUrl = `${baseUrl}${apiPrefix}${proxyServerUrl}`; } + // let newSseUrl = '' + // if (sseUrl.startsWith('http://') || sseUrl.startsWith('https://')) { + // // 如果是完整URL,直接使用 + // newSseUrl = sseUrl + // } else { + // // 如果是相对路径,添加基础URL和API前缀 + // const baseUrl = window.location.origin; + // const apiPrefix = '/api/v1/'; + // newSseUrl = `${baseUrl}${apiPrefix}${sseUrl}`; + // } const mcpProxyServerUrl = new URL(fullUrl); - mcpProxyServerUrl.searchParams.append("transportType", transportType); - if (transportType === "stdio") { - mcpProxyServerUrl.searchParams.append("command", command || ''); - mcpProxyServerUrl.searchParams.append("args", args || ''); - mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env || {})); - } else { - mcpProxyServerUrl.searchParams.append("url", sseUrl); - } - console.log('sseUrl===', sseUrl) + // mcpProxyServerUrl.searchParams.append("transportType", transportType); + // if (transportType === "stdio") { + // mcpProxyServerUrl.searchParams.append("command", command || ''); + // mcpProxyServerUrl.searchParams.append("args", args || ''); + // mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env || {})); + // } else { + // mcpProxyServerUrl.searchParams.append("url", newSseUrl); + // } + // console.log('sseUrl===', newSseUrl) try { // Inject auth manually instead of using SSEClientTransport, because we're // proxying through the inspector server first. From 7b9f7d5acd181c6c6bfbffac0796ffcfcf97afd5 Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Thu, 10 Apr 2025 16:10:24 +0800 Subject: [PATCH 5/6] feature/1.7-MCP --- .../mcpService/IntegrationAIContainer.tsx | 138 ++++++++++-------- .../src/pages/mcpService/McpKeyContainer.tsx | 84 ++++++----- 2 files changed, 123 insertions(+), 99 deletions(-) diff --git a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx index 248da0ee..d46a0bdb 100644 --- a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx +++ b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx @@ -1,4 +1,4 @@ -import { App, Card, Select } from 'antd' +import { App, Button, Card, Empty, Select } from 'antd' import { $t } from '@common/locales/index.ts' import { Icon } from '@iconify/react/dist/iconify.js' import { useEffect, useState } from 'react' @@ -8,7 +8,8 @@ import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' import { useConnection } from './hook/useConnection' import { ClientRequest, Tool, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' -import { z } from "zod"; +import { z } from 'zod' +import { useNavigate } from 'react-router-dom' type ConfigList = { openApi?: { @@ -30,19 +31,26 @@ type ApiKeyItem = { value: string } -const IntegrationAIContainer = ({ type, handleToolsChange }: { type: 'global' | 'service'; handleToolsChange: (value: Tool[]) => void }) => { +const IntegrationAIContainer = ({ + type, + handleToolsChange +}: { + type: 'global' | 'service' + handleToolsChange: (value: Tool[]) => void +}) => { const [activeTab, setActiveTab] = useState('mcp') const { message } = App.useApp() const [configContent, setConfigContent] = useState('') const [apiKey, setApiKey] = useState('') const [apiKeyList, setApiKeyList] = useState<{ value: string; label: string }[]>([]) const [mcpServerUrl, setMcpServerUrl] = useState('') + const navigator = useNavigate() const [errors, setErrors] = useState>({ resources: null, prompts: null, - tools: null, - }); - + tools: null + }) + const [tabContent, setTabContent] = useState({ mcp: { title: $t('MCP 配置'), @@ -113,6 +121,10 @@ const IntegrationAIContainer = ({ type, handleToolsChange }: { type: 'global' | }) } + const addKey = () => { + navigator('/mcpKey') + } + /** * 获取 API Key 列表 */ @@ -144,43 +156,39 @@ const IntegrationAIContainer = ({ type, handleToolsChange }: { type: 'global' | } const clearError = (tabKey: keyof typeof errors) => { - setErrors((prev) => ({ ...prev, [tabKey]: null })); - }; + setErrors((prev) => ({ ...prev, [tabKey]: null })) + } - const makeRequest = async ( - request: ClientRequest, - schema: T, - tabKey?: keyof typeof errors, - ) => { + const makeRequest = async (request: ClientRequest, schema: T, tabKey?: keyof typeof errors) => { try { - const response = await makeConnectionRequest(request, schema); + const response = await makeConnectionRequest(request, schema) if (tabKey !== undefined) { - clearError(tabKey); + clearError(tabKey) } - return response; + return response } catch (e) { - const errorString = (e as Error).message ?? String(e); + const errorString = (e as Error).message ?? String(e) if (tabKey !== undefined) { setErrors((prev) => ({ ...prev, - [tabKey]: errorString, - })); + [tabKey]: errorString + })) } - throw e; + throw e } - }; + } const listTools = async () => { const response = await makeRequest( { - method: "tools/list" as const, - params: {}, + method: 'tools/list' as const, + params: {} }, ListToolsResultSchema, - "tools", - ); + 'tools' + ) handleToolsChange(response.tools) - }; + } const { connectionStatus, @@ -192,13 +200,13 @@ const IntegrationAIContainer = ({ type, handleToolsChange }: { type: 'global' | handleCompletion, completionsSupported, connect: connectMcpServer, - disconnect: disconnectMcpServer, + disconnect: disconnectMcpServer } = useConnection({ transportType: 'sse', sseUrl: '', proxyServerUrl: mcpServerUrl, - requestTimeout: 1000, - }); + requestTimeout: 1000 + }) useEffect(() => { if (type === 'global') { @@ -209,12 +217,10 @@ const IntegrationAIContainer = ({ type, handleToolsChange }: { type: 'global' | getKeysList() }, []) useEffect(() => { - if (apiKey) { - if (activeTab === 'openApi' && tabContent?.openApi?.configContent) { - setConfigContent(tabContent?.openApi?.configContent?.replace('{your_api_key}', apiKey) || '') - } else if (activeTab === 'mcp' && tabContent?.mcp?.configContent) { - setConfigContent(tabContent.mcp.configContent?.replace('{your_api_key}', apiKey) || '') - } + if (activeTab === 'openApi' && tabContent?.openApi?.configContent) { + setConfigContent(tabContent?.openApi?.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}')) + } else if (activeTab === 'mcp' && tabContent?.mcp?.configContent) { + setConfigContent(tabContent.mcp.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}')) } }, [apiKey, activeTab, tabContent]) @@ -308,34 +314,42 @@ const IntegrationAIContainer = ({ type, handleToolsChange }: { type: 'global' | {activeTab === 'mcp' && ( <>
API Key
- +
-
- + > +
+ {apiKey} + handleCopy(configContent)} + sx={{ + position: 'absolute', + top: '0px', + right: '5px', + color: '#999', + transition: 'none', + '&.MuiButtonBase-root:hover': { + background: 'transparent', + color: '#3D46F2', + transition: 'none' + } + }} + > +
+ + + ) : ( + + + + )} )} diff --git a/frontend/packages/core/src/pages/mcpService/McpKeyContainer.tsx b/frontend/packages/core/src/pages/mcpService/McpKeyContainer.tsx index a7aaa5a6..31c73d41 100644 --- a/frontend/packages/core/src/pages/mcpService/McpKeyContainer.tsx +++ b/frontend/packages/core/src/pages/mcpService/McpKeyContainer.tsx @@ -1,7 +1,7 @@ import InsidePage from '@common/components/aoplatform/InsidePage' import { IconButton } from '@common/components/postcat/api/IconButton' import { $t } from '@common/locales/index.ts' -import { Button, Card, App } from 'antd' +import { Button, Card, App, Empty } from 'antd' import { useFetch } from '@common/hooks/http' import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useEffect, useRef, useState } from 'react' @@ -139,17 +139,36 @@ const McpKeyContainer = () => { {$t('新增 API Key')}
- {keys.map((key, index) => ( - -
-
-

{key.name}

-
- {key.value} + {keys.length ? ( + keys.map((key, index) => ( + +
+
+

{key.name}

+
+ {key.value} + { + copyCode(key?.value) + }} + sx={{ + color: '#333', + transition: 'none', + '&.MuiButtonBase-root:hover': { + background: 'transparent', + color: '#3D46F2', + transition: 'none' + } + }} + > +
+
+
{ - copyCode(key?.value) + editKey(key) }} sx={{ color: '#333', @@ -161,35 +180,26 @@ const McpKeyContainer = () => { } }} > + { + deleteKey(key.id) + }} + sx={{ + color: '#333', + transition: 'none', + '&.MuiButtonBase-root:hover': { background: 'transparent', color: 'red', transition: 'none' } + }} + >
-
- { - editKey(key) - }} - sx={{ - color: '#333', - transition: 'none', - '&.MuiButtonBase-root:hover': { background: 'transparent', color: '#3D46F2', transition: 'none' } - }} - > - { - deleteKey(key.id) - }} - sx={{ - color: '#333', - transition: 'none', - '&.MuiButtonBase-root:hover': { background: 'transparent', color: 'red', transition: 'none' } - }} - > -
-
- - ))} + + )) + ) : ( + <> + + + )}
From b7119fe248fbb5c9895d1dc8ee99315b2a0e39e3 Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Thu, 10 Apr 2025 16:22:26 +0800 Subject: [PATCH 6/6] feature/1.7-MCP --- frontend/packages/common/src/contexts/GlobalStateContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx index a56c9448..99087cd1 100644 --- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -204,7 +204,7 @@ const mockData = [ access: '' }, { - name: 'MCP Key', + name: 'API Key', key: 'mcpKey', path: '/mcpKey', icon: 'material-symbols:key',