From aae9fbb9deebc41d43e23b89aced975201a6b4fa Mon Sep 17 00:00:00 2001 From: balibabu Date: Thu, 10 Jul 2025 09:33:29 +0800 Subject: [PATCH] Feat: Test MCP server #3221 (#8757) ### What problem does this PR solve? Feat: Test MCP server #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/hooks/use-mcp-request.ts | 11 +-- web/src/interfaces/database/mcp.ts | 28 ++++++- web/src/interfaces/request/mcp.ts | 7 ++ .../profile-setting/mcp/edit-mcp-dialog.tsx | 74 ++++++++++++++++++- .../profile-setting/mcp/edit-mcp-form.tsx | 17 ++++- .../pages/profile-setting/mcp/mcp-card.tsx | 22 ++++-- .../pages/profile-setting/mcp/tool-card.tsx | 19 +++++ 7 files changed, 158 insertions(+), 20 deletions(-) create mode 100644 web/src/interfaces/request/mcp.ts create mode 100644 web/src/pages/profile-setting/mcp/tool-card.tsx diff --git a/web/src/hooks/use-mcp-request.ts b/web/src/hooks/use-mcp-request.ts index 522c35404..6cb2979e9 100644 --- a/web/src/hooks/use-mcp-request.ts +++ b/web/src/hooks/use-mcp-request.ts @@ -1,5 +1,6 @@ import message from '@/components/ui/message'; -import { IMcpServerListResponse } from '@/interfaces/database/mcp'; +import { IMcpServerListResponse, IMCPTool } from '@/interfaces/database/mcp'; +import { ITestMcpRequestBody } from '@/interfaces/request/mcp'; import i18n from '@/locales/config'; import mcpServerService from '@/services/mcp-server-service'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -164,12 +165,12 @@ export const useTestMcpServer = () => { data, isPending: loading, mutateAsync, - } = useMutation({ + } = useMutation({ mutationKey: [McpApiAction.TestMcpServer], - mutationFn: async (params: Record) => { - const { data = {} } = await mcpServerService.test(params); + mutationFn: async (params) => { + const { data } = await mcpServerService.test(params); - return data; + return data?.data || []; }, }); diff --git a/web/src/interfaces/database/mcp.ts b/web/src/interfaces/database/mcp.ts index 7bec45cc5..d90699801 100644 --- a/web/src/interfaces/database/mcp.ts +++ b/web/src/interfaces/database/mcp.ts @@ -6,10 +6,36 @@ export interface IMcpServer { server_type: string; update_date: string; url: string; - variables: Record; + variables: Record & { tools?: IMCPToolObject }; } +export type IMCPToolObject = Record>; + export interface IMcpServerListResponse { mcp_servers: IMcpServer[]; total: number; } + +export interface IMCPTool { + annotations: null; + description: string; + enabled: boolean; + inputSchema: InputSchema; + name: string; +} + +interface InputSchema { + properties: Properties; + required: string[]; + title: string; + type: string; +} + +interface Properties { + symbol: ISymbol; +} + +interface ISymbol { + title: string; + type: string; +} diff --git a/web/src/interfaces/request/mcp.ts b/web/src/interfaces/request/mcp.ts new file mode 100644 index 000000000..a63d3b0f4 --- /dev/null +++ b/web/src/interfaces/request/mcp.ts @@ -0,0 +1,7 @@ +export interface ITestMcpRequestBody { + server_type: string; + url: string; + headers?: Record; + variables?: Record; + timeout?: number; +} diff --git a/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx b/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx index ad98c6ff3..69687a81f 100644 --- a/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx +++ b/web/src/pages/profile-setting/mcp/edit-mcp-dialog.tsx @@ -1,4 +1,5 @@ -import { ButtonLoading } from '@/components/ui/button'; +import { Collapse } from '@/components/collapse'; +import { Button, ButtonLoading } from '@/components/ui/button'; import { Dialog, DialogContent, @@ -6,12 +7,52 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog'; +import { useTestMcpServer } from '@/hooks/use-mcp-request'; import { IModalProps } from '@/interfaces/common'; +import { IMCPTool, IMCPToolObject } from '@/interfaces/database/mcp'; +import { omit } from 'lodash'; +import { RefreshCw } from 'lucide-react'; +import { MouseEventHandler, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { EditMcpForm, FormId } from './edit-mcp-form'; +import { z } from 'zod'; +import { EditMcpForm, FormId, useBuildFormSchema } from './edit-mcp-form'; +import { McpToolCard } from './tool-card'; + +function transferToolToObject(tools: IMCPTool[] = []) { + return tools.reduce((pre, tool) => { + pre[tool.name] = omit(tool, 'name'); + return pre; + }, {}); +} export function EditMcpDialog({ hideModal, loading, onOk }: IModalProps) { const { t } = useTranslation(); + const { testMcpServer, data: tools } = useTestMcpServer(); + const [isTriggeredBySaving, setIsTriggeredBySaving] = useState(false); + const FormSchema = useBuildFormSchema(); + + const handleTest: MouseEventHandler = useCallback((e) => { + e.stopPropagation(); + setIsTriggeredBySaving(false); + }, []); + + const handleSave: MouseEventHandler = useCallback(() => { + setIsTriggeredBySaving(true); + }, []); + + const handleOk = async (values: z.infer) => { + if (isTriggeredBySaving) { + onOk?.({ + ...values, + variables: { + ...(values?.variables || {}), + tools: transferToolToObject(tools), + }, + }); + } else { + testMcpServer(values); + } + }; return ( @@ -19,9 +60,34 @@ export function EditMcpDialog({ hideModal, loading, onOk }: IModalProps) { Edit profile - + + {tools?.length || 0} tools available} + rightContent={ + + } + > +
+ {tools?.map((x) => ( + + ))} +
+
- + {t('common.save')} diff --git a/web/src/pages/profile-setting/mcp/edit-mcp-form.tsx b/web/src/pages/profile-setting/mcp/edit-mcp-form.tsx index 8daa6f494..e71cdf631 100644 --- a/web/src/pages/profile-setting/mcp/edit-mcp-form.tsx +++ b/web/src/pages/profile-setting/mcp/edit-mcp-form.tsx @@ -28,10 +28,7 @@ enum ServerType { const ServerTypeOptions = buildOptions(ServerType); -export function EditMcpForm({ - initialName, - onOk, -}: IModalProps & { initialName?: string }) { +export function useBuildFormSchema() { const { t } = useTranslation(); const FormSchema = z.object({ @@ -53,8 +50,20 @@ export function EditMcpForm({ message: t('common.namePlaceholder'), }) .trim(), + variables: z.object({}).optional(), }); + return FormSchema; +} + +export function EditMcpForm({ + initialName, + onOk, +}: IModalProps & { initialName?: string }) { + const { t } = useTranslation(); + + const FormSchema = useBuildFormSchema(); + const form = useForm>({ resolver: zodResolver(FormSchema), defaultValues: { name: '', server_type: ServerType.SSE, url: '' }, diff --git a/web/src/pages/profile-setting/mcp/mcp-card.tsx b/web/src/pages/profile-setting/mcp/mcp-card.tsx index 07eded047..1c154d865 100644 --- a/web/src/pages/profile-setting/mcp/mcp-card.tsx +++ b/web/src/pages/profile-setting/mcp/mcp-card.tsx @@ -3,6 +3,8 @@ import { Card, CardContent } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; import { IMcpServer } from '@/interfaces/database/mcp'; import { formatDate } from '@/utils/date'; +import { isPlainObject } from 'lodash'; +import { useMemo } from 'react'; import { McpDropdown } from './mcp-dropdown'; import { UseBulkOperateMCPReturnType } from './use-bulk-operate-mcp'; @@ -15,6 +17,18 @@ export function McpCard({ selectedList, handleSelectChange, }: DatasetCardProps) { + const toolLength = useMemo(() => { + const tools = data.variables?.tools; + if (isPlainObject(tools)) { + return Object.keys(tools || {}).length; + } + return 0; + }, [data.variables?.tools]); + const onCheckedChange = (checked: boolean) => { + if (typeof checked === 'boolean') { + handleSelectChange(data.id, checked); + } + }; return ( @@ -26,11 +40,7 @@ export function McpCard({ { - if (typeof checked === 'boolean') { - handleSelectChange(data.id, checked); - } - }} + onCheckedChange={onCheckedChange} onClick={(e) => { e.stopPropagation(); }} @@ -40,7 +50,7 @@ export function McpCard({
- 20 cached tools + {toolLength} cached tools

{formatDate(data.update_date)} diff --git a/web/src/pages/profile-setting/mcp/tool-card.tsx b/web/src/pages/profile-setting/mcp/tool-card.tsx new file mode 100644 index 000000000..f1b15d8b0 --- /dev/null +++ b/web/src/pages/profile-setting/mcp/tool-card.tsx @@ -0,0 +1,19 @@ +import { Card, CardContent } from '@/components/ui/card'; +import { IMCPTool } from '@/interfaces/database/mcp'; + +export type McpToolCardProps = { + data: IMCPTool; +}; + +export function McpToolCard({ data }: McpToolCardProps) { + return ( + + +

{data.name}

+
+ {data.description} +
+ + + ); +}