From f683580310505fa3bef0a867c8517eb1290d0038 Mon Sep 17 00:00:00 2001 From: balibabu Date: Tue, 15 Jul 2025 09:37:08 +0800 Subject: [PATCH] Feat: Synchronize MCP data to agent #3221 (#8832) ### What problem does this PR solve? Feat: Synchronize MCP data to agent #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/hooks/use-mcp-request.ts | 14 ++- web/src/interfaces/database/mcp.ts | 2 + web/src/pages/agent/form-sheet/next.tsx | 39 +++++-- web/src/pages/agent/form/tool-form/index.tsx | 2 + .../agent/form/tool-form/mcp-form/index.tsx | 103 ++++++++++++++++++ .../form/tool-form/mcp-form/mcp-card.tsx | 20 ++++ .../form/tool-form/mcp-form/use-values.ts | 26 +++++ .../tool-form/mcp-form/use-watch-change.ts | 47 ++++++++ web/src/pages/agent/utils.ts | 5 + web/src/services/mcp-server-service.ts | 2 +- 10 files changed, 242 insertions(+), 18 deletions(-) create mode 100644 web/src/pages/agent/form/tool-form/mcp-form/mcp-card.tsx create mode 100644 web/src/pages/agent/form/tool-form/mcp-form/use-values.ts create mode 100644 web/src/pages/agent/form/tool-form/mcp-form/use-watch-change.ts diff --git a/web/src/hooks/use-mcp-request.ts b/web/src/hooks/use-mcp-request.ts index ac0922243..64c25e5d6 100644 --- a/web/src/hooks/use-mcp-request.ts +++ b/web/src/hooks/use-mcp-request.ts @@ -5,6 +5,7 @@ import { IMcpServer, IMcpServerListResponse, IMCPTool, + IMCPToolRecord, } from '@/interfaces/database/mcp'; import { IImportMcpServersRequestBody, @@ -16,6 +17,7 @@ import mcpServerService, { } from '@/services/mcp-server-service'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useDebounce } from 'ahooks'; +import { useState } from 'react'; import { useGetPaginationWithRouter, useHandleSearchChange, @@ -201,17 +203,19 @@ export const useExportMcpServer = () => { }; export const useListMcpServerTools = () => { - const { data, isFetching: loading } = useQuery({ + const [ids, setIds] = useState([]); + const { data, isFetching: loading } = useQuery({ queryKey: [McpApiAction.ListMcpServerTools], - initialData: [], + initialData: {} as IMCPToolRecord, gcTime: 0, + enabled: ids.length > 0, queryFn: async () => { - const { data } = await mcpServerService.listTools(); - return data?.data ?? []; + const { data } = await mcpServerService.listTools({ mcp_ids: ids }); + return data?.data ?? {}; }, }); - return { data, loading }; + return { data, loading, setIds }; }; export const useTestMcpServer = () => { diff --git a/web/src/interfaces/database/mcp.ts b/web/src/interfaces/database/mcp.ts index 6edbf6a06..143cf8cb4 100644 --- a/web/src/interfaces/database/mcp.ts +++ b/web/src/interfaces/database/mcp.ts @@ -11,6 +11,8 @@ export interface IMcpServer { export type IMCPToolObject = Record>; +export type IMCPToolRecord = Record; + export interface IMcpServerListResponse { mcp_servers: IMcpServer[]; total: number; diff --git a/web/src/pages/agent/form-sheet/next.tsx b/web/src/pages/agent/form-sheet/next.tsx index d707e219b..1f08e1540 100644 --- a/web/src/pages/agent/form-sheet/next.tsx +++ b/web/src/pages/agent/form-sheet/next.tsx @@ -11,11 +11,13 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { cn } from '@/lib/utils'; import { lowerFirst } from 'lodash'; import { Play, X } from 'lucide-react'; +import { useMemo } from 'react'; import { BeginId, Operator } from '../constant'; import { AgentFormContext } from '../context'; import { RunTooltip } from '../flow-tooltip'; import { useHandleNodeNameChange } from '../hooks/use-change-node-name'; import OperatorIcon from '../operator-icon'; +import useGraphStore from '../store'; import { needsSingleStepDebugging } from '../utils'; import SingleDebugDrawer from './single-debug-drawer'; import { useFormConfigMap } from './use-form-config-map'; @@ -40,6 +42,7 @@ const FormSheet = ({ showSingleDebugDrawer, }: IModalProps & IProps) => { const operatorName: Operator = node?.data.label as Operator; + const clickedToolId = useGraphStore((state) => state.clickedToolId); const FormConfigMap = useFormConfigMap(); @@ -52,6 +55,13 @@ const FormSheet = ({ data: node?.data, }); + const isMcp = useMemo(() => { + return ( + operatorName === Operator.Tool && + Object.values(Operator).every((x) => x !== clickedToolId) + ); + }, [clickedToolId, operatorName]); + const { t } = useTranslate('flow'); return ( @@ -67,18 +77,23 @@ const FormSheet = ({
-
- - {node?.id === BeginId ? ( - {t(BeginId)} - ) : ( - - )} -
+ + {isMcp ? ( +
MCP Config
+ ) : ( +
+ + {node?.id === BeginId ? ( + {t(BeginId)} + ) : ( + + )} +
+ )} {needsSingleStepDebugging(operatorName) && ( diff --git a/web/src/pages/agent/form/tool-form/index.tsx b/web/src/pages/agent/form/tool-form/index.tsx index c23ab05e4..7bc68d127 100644 --- a/web/src/pages/agent/form/tool-form/index.tsx +++ b/web/src/pages/agent/form/tool-form/index.tsx @@ -1,5 +1,6 @@ import useGraphStore from '../../store'; import { ToolFormConfigMap } from './constant'; +import MCPForm from './mcp-form'; const EmptyContent = () =>
; @@ -8,6 +9,7 @@ const ToolForm = () => { const ToolForm = ToolFormConfigMap[clickedToolId as keyof typeof ToolFormConfigMap] ?? + MCPForm ?? EmptyContent; return ( diff --git a/web/src/pages/agent/form/tool-form/mcp-form/index.tsx b/web/src/pages/agent/form/tool-form/mcp-form/index.tsx index e69de29bb..9fd0802dc 100644 --- a/web/src/pages/agent/form/tool-form/mcp-form/index.tsx +++ b/web/src/pages/agent/form/tool-form/mcp-form/index.tsx @@ -0,0 +1,103 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { useGetMcpServer } from '@/hooks/use-mcp-request'; +import useGraphStore from '@/pages/agent/store'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { MCPCard } from './mcp-card'; +import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-change'; + +const FormSchema = z.object({ + items: z.array(z.string()), +}); + +function MCPForm() { + const clickedToolId = useGraphStore((state) => state.clickedToolId); + const values = useValues(); + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + const { data } = useGetMcpServer(clickedToolId); + + useWatchFormChange(form); + + return ( +
+ { + e.preventDefault(); + }} + > + + + {data.name} + + + URL: + + {data.url} + + + + ( + + {Object.entries(data.variables?.tools || {}).map( + ([name, mcp]) => ( + { + return ( + + + + { + return checked + ? field.onChange([...field.value, name]) + : field.onChange( + field.value?.filter( + (value) => value !== name, + ), + ); + }} + /> + + + + ); + }} + /> + ), + )} + + + )} + /> + + + ); +} + +export default memo(MCPForm); diff --git a/web/src/pages/agent/form/tool-form/mcp-form/mcp-card.tsx b/web/src/pages/agent/form/tool-form/mcp-form/mcp-card.tsx new file mode 100644 index 000000000..d541fea33 --- /dev/null +++ b/web/src/pages/agent/form/tool-form/mcp-form/mcp-card.tsx @@ -0,0 +1,20 @@ +import { Card, CardContent, CardTitle } from '@/components/ui/card'; +import { IMCPTool } from '@/interfaces/database/mcp'; +import { PropsWithChildren } from 'react'; + +export function MCPCard({ + data, + children, +}: { data: IMCPTool } & PropsWithChildren) { + return ( + + + {children} +
+ {data.name} +

{data.description}

+
+
+
+ ); +} diff --git a/web/src/pages/agent/form/tool-form/mcp-form/use-values.ts b/web/src/pages/agent/form/tool-form/mcp-form/use-values.ts new file mode 100644 index 000000000..ae97de681 --- /dev/null +++ b/web/src/pages/agent/form/tool-form/mcp-form/use-values.ts @@ -0,0 +1,26 @@ +import useGraphStore from '@/pages/agent/store'; +import { getAgentNodeMCP } from '@/pages/agent/utils'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; + +export function useValues() { + const { clickedToolId, clickedNodeId, findUpstreamNodeById } = useGraphStore( + (state) => state, + ); + + const values = useMemo(() => { + const agentNode = findUpstreamNodeById(clickedNodeId); + const mcpList = getAgentNodeMCP(agentNode); + + const formData = + mcpList.find((x) => x.mcp_id === clickedToolId)?.tools || {}; + + if (isEmpty(formData)) { + return { items: [] }; + } + + return { items: Object.keys(formData) }; + }, [clickedNodeId, clickedToolId, findUpstreamNodeById]); + + return values; +} diff --git a/web/src/pages/agent/form/tool-form/mcp-form/use-watch-change.ts b/web/src/pages/agent/form/tool-form/mcp-form/use-watch-change.ts new file mode 100644 index 000000000..fa3de056d --- /dev/null +++ b/web/src/pages/agent/form/tool-form/mcp-form/use-watch-change.ts @@ -0,0 +1,47 @@ +import { useGetMcpServer } from '@/hooks/use-mcp-request'; +import useGraphStore from '@/pages/agent/store'; +import { getAgentNodeMCP } from '@/pages/agent/utils'; +import { pick } from 'lodash'; +import { useEffect, useMemo } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; + +export function useWatchFormChange(form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const { clickedToolId, clickedNodeId, findUpstreamNodeById, updateNodeForm } = + useGraphStore((state) => state); + const { data } = useGetMcpServer(clickedToolId); + + const nextMCPTools = useMemo(() => { + const mcpTools = data.variables?.tools || []; + values = form?.getValues(); + + return pick(mcpTools, values.items); + }, [values, data?.variables]); + + useEffect(() => { + const agentNode = findUpstreamNodeById(clickedNodeId); + // Manually triggered form updates are synchronized to the canvas + if (agentNode) { + const agentNodeId = agentNode?.id; + const mcpList = getAgentNodeMCP(agentNode); + + const nextMCP = mcpList.map((x) => { + if (x.mcp_id === clickedToolId) { + return { + ...x, + tools: nextMCPTools, + }; + } + return x; + }); + + updateNodeForm(agentNodeId, nextMCP, ['mcp']); + } + }, [ + clickedNodeId, + clickedToolId, + findUpstreamNodeById, + nextMCPTools, + updateNodeForm, + ]); +} diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index e2e064487..a90eafc3d 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -569,6 +569,11 @@ export function getAgentNodeTools(agentNode?: RAGFlowNodeType) { return tools; } +export function getAgentNodeMCP(agentNode?: RAGFlowNodeType) { + const tools: IAgentForm['mcp'] = get(agentNode, 'data.form.mcp', []); + return tools; +} + export function mapEdgeMouseEvent( edges: Edge[], edgeId: string, diff --git a/web/src/services/mcp-server-service.ts b/web/src/services/mcp-server-service.ts index 3bad79325..55c72116e 100644 --- a/web/src/services/mcp-server-service.ts +++ b/web/src/services/mcp-server-service.ts @@ -48,7 +48,7 @@ const methods = { }, listTools: { url: listMcpServerTools, - method: 'get', + method: 'post', }, testTool: { url: testMcpServerTool,