From bed412d1e2fd6d3b2ac5b7ddd6bcb9be4d6e6d33 Mon Sep 17 00:00:00 2001 From: jZonG Date: Thu, 8 May 2025 15:51:19 +0800 Subject: [PATCH] MCP create --- web/app/components/tools/mcp/create-card.tsx | 13 ++ web/app/components/tools/mcp/index.tsx | 4 +- web/app/components/tools/mcp/modal.tsx | 131 +++++++++++++++++++ web/i18n/en-US/tools.ts | 9 ++ web/i18n/zh-Hans/tools.ts | 9 ++ web/service/use-tools.ts | 27 ++++ 6 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 web/app/components/tools/mcp/modal.tsx diff --git a/web/app/components/tools/mcp/create-card.tsx b/web/app/components/tools/mcp/create-card.tsx index 51cd03ba61..2896372450 100644 --- a/web/app/components/tools/mcp/create-card.tsx +++ b/web/app/components/tools/mcp/create-card.tsx @@ -7,9 +7,11 @@ import { RiArrowRightUpLine, RiBookOpenLine, } from '@remixicon/react' +import MCPModal from './modal' import I18n from '@/context/i18n' import { getLanguage } from '@/i18n/language' import { useAppContext } from '@/context/app-context' +import { useCreateMCP } from '@/service/use-tools' type Props = { handleCreate: () => void @@ -21,6 +23,10 @@ const NewMCPCard = ({ handleCreate }: Props) => { const language = getLanguage(locale) const { isCurrentWorkspaceManager } = useAppContext() + const { mutate: createMCP } = useCreateMCP({ + onSuccess: handleCreate, + }) + const linkUrl = useMemo(() => { // TODO help link if (language.startsWith('zh_')) @@ -51,6 +57,13 @@ const NewMCPCard = ({ handleCreate }: Props) => { )} + {showModal && ( + setShowModal(false)} + /> + )} ) } diff --git a/web/app/components/tools/mcp/index.tsx b/web/app/components/tools/mcp/index.tsx index 557d187562..2fe9c86fa6 100644 --- a/web/app/components/tools/mcp/index.tsx +++ b/web/app/components/tools/mcp/index.tsx @@ -4,7 +4,7 @@ import NewMCPCard from './create-card' import MCPCard from './provider-card' import MCPDetailPanel from './provider-detail' import { useAllMCPTools, useInvalidateAllMCPTools } from '@/service/use-tools' -import type { MCPProvider } from '@/app/components/tools/types' +import type { ToolWithProvider } from '@/app/components/workflow/types' import cn from '@/utils/classnames' type Props = { @@ -43,7 +43,7 @@ const MCPList = ({ }) }, [list, searchText]) - const [currentProvider, setCurrentProvider] = useState() + const [currentProvider, setCurrentProvider] = useState() return ( <> diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx new file mode 100644 index 0000000000..d1a652de04 --- /dev/null +++ b/web/app/components/tools/mcp/modal.tsx @@ -0,0 +1,131 @@ +'use client' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RiCloseLine } from '@remixicon/react' +import AppIconPicker from '@/app/components/base/app-icon-picker' +import type { AppIconSelection } from '@/app/components/base/app-icon-picker' +import AppIcon from '@/app/components/base/app-icon' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import type { AppIconType } from '@/types/app' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import { noop } from 'lodash-es' +import cn from '@/utils/classnames' + +export type DuplicateAppModalProps = { + data?: ToolWithProvider + show: boolean + onConfirm: (info: { + name: string + server_url: string + icon_type: AppIconType + icon: string + icon_background?: string | null + }) => void + onHide: () => void +} + +const DEFAULT_ICON = { type: 'emoji', icon: '🧿', background: '#EFF1F5' } +const extractFileId = (url: string) => { + const match = url.match(/files\/(.+?)\/file-preview/) + return match ? match[1] : null +} +const getIcon = (data?: ToolWithProvider) => { + if (!data) + return DEFAULT_ICON as AppIconSelection + if (typeof data.icon === 'string') + return { type: 'image', url: data.icon, fileId: extractFileId(data.icon) } as AppIconSelection + return data.icon as unknown as AppIconSelection +} + +const MCPModal = ({ + data, + show, + onConfirm, + onHide, +}: DuplicateAppModalProps) => { + const { t } = useTranslation() + + const [name, setName] = React.useState(data?.name || '') + const [appIcon, setAppIcon] = useState(getIcon(data)) + const [url, setUrl] = React.useState(data?.server_url || '') + const [showAppIconPicker, setShowAppIconPicker] = useState(false) + + const submit = async () => { + await onConfirm({ + name, + server_url: url, + icon_type: appIcon.type, + icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, + icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, + }) + onHide() + } + + return ( + <> + +
+ +
+
{t('tools.mcp.modal.title')}
+
+
+
+
+ {t('tools.mcp.modal.name')} +
+ setName(e.target.value)} + placeholder={t('tools.mcp.modal.namePlaceholder')} + /> +
+
+ { setShowAppIconPicker(true) }} + /> +
+
+
+
+ {t('tools.mcp.modal.serverUrl')} +
+ setUrl(e.target.value)} + placeholder={t('tools.mcp.modal.serverUrlPlaceholder')} + /> +
+
+
+ + +
+
+ {showAppIconPicker && { + setAppIcon(payload) + setShowAppIconPicker(false) + }} + onClose={() => { + setAppIcon(getIcon(data)) + setShowAppIconPicker(false) + }} + />} + + + ) +} + +export default MCPModal diff --git a/web/i18n/en-US/tools.ts b/web/i18n/en-US/tools.ts index 624819efae..1954f264f3 100644 --- a/web/i18n/en-US/tools.ts +++ b/web/i18n/en-US/tools.ts @@ -162,6 +162,15 @@ const translation = { updateTime: 'Updated', toolsCount: '{{count}} tools', noTools: 'No tools available', + modal: { + title: 'Add MCP Server (HTTP)', + name: 'Name & Icon', + namePlaceholder: 'Name your MCP server', + serverUrl: 'Server URL', + serverUrlPlaceholder: 'URL to server endpiont', + cancel: 'Cancel', + confirm: 'Add & Authorize', + }, }, } diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 4f8dcc09e2..1c5b5a81e1 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -162,6 +162,15 @@ const translation = { updateTime: '更新于', toolsCount: '{{count}} 个工具', noTools: '没有可用的工具', + modal: { + title: '添加 MCP 服务 (HTTP)', + name: '名称和图标', + namePlaceholder: '命名你的 MCP 服务', + serverUrl: '服务端点 URL', + serverUrlPlaceholder: '服务端点的 URL', + cancel: '取消', + confirm: '添加并授权', + }, }, } diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index 9a61a0792d..78797a2cb3 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -4,6 +4,7 @@ import type { Tool, } from '@/app/components/tools/types' import type { ToolWithProvider } from '@/app/components/workflow/types' +import type { AppIconType } from '@/types/app' import { useInvalid } from './use-base' import { useMutation, @@ -78,6 +79,32 @@ export const useInvalidateAllMCPTools = () => { return useInvalid(useAllMCPToolsKey) } +export const useCreateMCP = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'create-mcp'], + mutationFn: (payload: { + name: string + server_url: string + icon_type: AppIconType + icon: string + icon_background?: string | null + }) => { + console.log('payload', payload) + return Promise.resolve(payload) + // return post('/console/api/workspaces/current/tool-provider/mcp', { + // body: { + // ...payload, + // }, + // }) + }, + onSuccess, + }) +} + export const useBuiltinProviderInfo = (providerName: string) => { return useQuery({ queryKey: [NAME_SPACE, 'builtin-provider-info', providerName],