mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### 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)
This commit is contained in:
@ -5,6 +5,7 @@ import {
|
|||||||
IMcpServer,
|
IMcpServer,
|
||||||
IMcpServerListResponse,
|
IMcpServerListResponse,
|
||||||
IMCPTool,
|
IMCPTool,
|
||||||
|
IMCPToolRecord,
|
||||||
} from '@/interfaces/database/mcp';
|
} from '@/interfaces/database/mcp';
|
||||||
import {
|
import {
|
||||||
IImportMcpServersRequestBody,
|
IImportMcpServersRequestBody,
|
||||||
@ -16,6 +17,7 @@ import mcpServerService, {
|
|||||||
} from '@/services/mcp-server-service';
|
} from '@/services/mcp-server-service';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useDebounce } from 'ahooks';
|
import { useDebounce } from 'ahooks';
|
||||||
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
useGetPaginationWithRouter,
|
useGetPaginationWithRouter,
|
||||||
useHandleSearchChange,
|
useHandleSearchChange,
|
||||||
@ -201,17 +203,19 @@ export const useExportMcpServer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useListMcpServerTools = () => {
|
export const useListMcpServerTools = () => {
|
||||||
const { data, isFetching: loading } = useQuery({
|
const [ids, setIds] = useState<string[]>([]);
|
||||||
|
const { data, isFetching: loading } = useQuery<IMCPToolRecord>({
|
||||||
queryKey: [McpApiAction.ListMcpServerTools],
|
queryKey: [McpApiAction.ListMcpServerTools],
|
||||||
initialData: [],
|
initialData: {} as IMCPToolRecord,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
|
enabled: ids.length > 0,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await mcpServerService.listTools();
|
const { data } = await mcpServerService.listTools({ mcp_ids: ids });
|
||||||
return data?.data ?? [];
|
return data?.data ?? {};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data, loading };
|
return { data, loading, setIds };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTestMcpServer = () => {
|
export const useTestMcpServer = () => {
|
||||||
|
|||||||
@ -11,6 +11,8 @@ export interface IMcpServer {
|
|||||||
|
|
||||||
export type IMCPToolObject = Record<string, Omit<IMCPTool, 'name'>>;
|
export type IMCPToolObject = Record<string, Omit<IMCPTool, 'name'>>;
|
||||||
|
|
||||||
|
export type IMCPToolRecord = Record<string, IMCPTool>;
|
||||||
|
|
||||||
export interface IMcpServerListResponse {
|
export interface IMcpServerListResponse {
|
||||||
mcp_servers: IMcpServer[];
|
mcp_servers: IMcpServer[];
|
||||||
total: number;
|
total: number;
|
||||||
|
|||||||
@ -11,11 +11,13 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { lowerFirst } from 'lodash';
|
import { lowerFirst } from 'lodash';
|
||||||
import { Play, X } from 'lucide-react';
|
import { Play, X } from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { BeginId, Operator } from '../constant';
|
import { BeginId, Operator } from '../constant';
|
||||||
import { AgentFormContext } from '../context';
|
import { AgentFormContext } from '../context';
|
||||||
import { RunTooltip } from '../flow-tooltip';
|
import { RunTooltip } from '../flow-tooltip';
|
||||||
import { useHandleNodeNameChange } from '../hooks/use-change-node-name';
|
import { useHandleNodeNameChange } from '../hooks/use-change-node-name';
|
||||||
import OperatorIcon from '../operator-icon';
|
import OperatorIcon from '../operator-icon';
|
||||||
|
import useGraphStore from '../store';
|
||||||
import { needsSingleStepDebugging } from '../utils';
|
import { needsSingleStepDebugging } from '../utils';
|
||||||
import SingleDebugDrawer from './single-debug-drawer';
|
import SingleDebugDrawer from './single-debug-drawer';
|
||||||
import { useFormConfigMap } from './use-form-config-map';
|
import { useFormConfigMap } from './use-form-config-map';
|
||||||
@ -40,6 +42,7 @@ const FormSheet = ({
|
|||||||
showSingleDebugDrawer,
|
showSingleDebugDrawer,
|
||||||
}: IModalProps<any> & IProps) => {
|
}: IModalProps<any> & IProps) => {
|
||||||
const operatorName: Operator = node?.data.label as Operator;
|
const operatorName: Operator = node?.data.label as Operator;
|
||||||
|
const clickedToolId = useGraphStore((state) => state.clickedToolId);
|
||||||
|
|
||||||
const FormConfigMap = useFormConfigMap();
|
const FormConfigMap = useFormConfigMap();
|
||||||
|
|
||||||
@ -52,6 +55,13 @@ const FormSheet = ({
|
|||||||
data: node?.data,
|
data: node?.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isMcp = useMemo(() => {
|
||||||
|
return (
|
||||||
|
operatorName === Operator.Tool &&
|
||||||
|
Object.values(Operator).every((x) => x !== clickedToolId)
|
||||||
|
);
|
||||||
|
}, [clickedToolId, operatorName]);
|
||||||
|
|
||||||
const { t } = useTranslate('flow');
|
const { t } = useTranslate('flow');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -67,18 +77,23 @@ const FormSheet = ({
|
|||||||
<section className="flex-col border-b py-2 px-5">
|
<section className="flex-col border-b py-2 px-5">
|
||||||
<div className="flex items-center gap-2 pb-3">
|
<div className="flex items-center gap-2 pb-3">
|
||||||
<OperatorIcon name={operatorName}></OperatorIcon>
|
<OperatorIcon name={operatorName}></OperatorIcon>
|
||||||
<div className="flex items-center gap-1 flex-1">
|
|
||||||
<label htmlFor="">{t('title')}</label>
|
{isMcp ? (
|
||||||
{node?.id === BeginId ? (
|
<div className="flex-1">MCP Config</div>
|
||||||
<span>{t(BeginId)}</span>
|
) : (
|
||||||
) : (
|
<div className="flex items-center gap-1 flex-1">
|
||||||
<Input
|
<label htmlFor="">{t('title')}</label>
|
||||||
value={name}
|
{node?.id === BeginId ? (
|
||||||
onBlur={handleNameBlur}
|
<span>{t(BeginId)}</span>
|
||||||
onChange={handleNameChange}
|
) : (
|
||||||
></Input>
|
<Input
|
||||||
)}
|
value={name}
|
||||||
</div>
|
onBlur={handleNameBlur}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
></Input>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{needsSingleStepDebugging(operatorName) && (
|
{needsSingleStepDebugging(operatorName) && (
|
||||||
<RunTooltip>
|
<RunTooltip>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import useGraphStore from '../../store';
|
import useGraphStore from '../../store';
|
||||||
import { ToolFormConfigMap } from './constant';
|
import { ToolFormConfigMap } from './constant';
|
||||||
|
import MCPForm from './mcp-form';
|
||||||
|
|
||||||
const EmptyContent = () => <div></div>;
|
const EmptyContent = () => <div></div>;
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ const ToolForm = () => {
|
|||||||
|
|
||||||
const ToolForm =
|
const ToolForm =
|
||||||
ToolFormConfigMap[clickedToolId as keyof typeof ToolFormConfigMap] ??
|
ToolFormConfigMap[clickedToolId as keyof typeof ToolFormConfigMap] ??
|
||||||
|
MCPForm ??
|
||||||
EmptyContent;
|
EmptyContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
className="space-y-6 p-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card className="bg-background-highlight p-5">
|
||||||
|
<CardHeader className="p-0 pb-3">
|
||||||
|
<CardTitle>{data.name}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<span className="pr-2"> URL:</span>
|
||||||
|
<a href={data.url} className="text-background-checked">
|
||||||
|
{data.url}
|
||||||
|
</a>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="items"
|
||||||
|
render={() => (
|
||||||
|
<FormItem className="space-y-2">
|
||||||
|
{Object.entries(data.variables?.tools || {}).map(
|
||||||
|
([name, mcp]) => (
|
||||||
|
<FormField
|
||||||
|
key={name}
|
||||||
|
control={form.control}
|
||||||
|
name="items"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem
|
||||||
|
key={name}
|
||||||
|
className="flex flex-row items-center gap-2"
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<MCPCard key={name} data={{ ...mcp, name }}>
|
||||||
|
<Checkbox
|
||||||
|
className="translate-y-1"
|
||||||
|
checked={field.value?.includes(name)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
return checked
|
||||||
|
? field.onChange([...field.value, name])
|
||||||
|
: field.onChange(
|
||||||
|
field.value?.filter(
|
||||||
|
(value) => value !== name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MCPCard>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(MCPForm);
|
||||||
|
|||||||
20
web/src/pages/agent/form/tool-form/mcp-form/mcp-card.tsx
Normal file
20
web/src/pages/agent/form/tool-form/mcp-form/mcp-card.tsx
Normal file
@ -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 (
|
||||||
|
<Card className="p-3">
|
||||||
|
<CardContent className="p-0 flex gap-3">
|
||||||
|
{children}
|
||||||
|
<section>
|
||||||
|
<CardTitle className="pb-3">{data.name}</CardTitle>
|
||||||
|
<p>{data.description}</p>
|
||||||
|
</section>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
web/src/pages/agent/form/tool-form/mcp-form/use-values.ts
Normal file
26
web/src/pages/agent/form/tool-form/mcp-form/use-values.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -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<any>) {
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
@ -569,6 +569,11 @@ export function getAgentNodeTools(agentNode?: RAGFlowNodeType) {
|
|||||||
return tools;
|
return tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAgentNodeMCP(agentNode?: RAGFlowNodeType) {
|
||||||
|
const tools: IAgentForm['mcp'] = get(agentNode, 'data.form.mcp', []);
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
export function mapEdgeMouseEvent(
|
export function mapEdgeMouseEvent(
|
||||||
edges: Edge[],
|
edges: Edge[],
|
||||||
edgeId: string,
|
edgeId: string,
|
||||||
|
|||||||
@ -48,7 +48,7 @@ const methods = {
|
|||||||
},
|
},
|
||||||
listTools: {
|
listTools: {
|
||||||
url: listMcpServerTools,
|
url: listMcpServerTools,
|
||||||
method: 'get',
|
method: 'post',
|
||||||
},
|
},
|
||||||
testTool: {
|
testTool: {
|
||||||
url: testMcpServerTool,
|
url: testMcpServerTool,
|
||||||
|
|||||||
Reference in New Issue
Block a user