mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Feat: Import and export MCP Server #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -1,10 +1,15 @@
|
|||||||
import message from '@/components/ui/message';
|
import message from '@/components/ui/message';
|
||||||
|
import { ResponseType } from '@/interfaces/database/base';
|
||||||
import {
|
import {
|
||||||
|
IExportedMcpServers,
|
||||||
IMcpServer,
|
IMcpServer,
|
||||||
IMcpServerListResponse,
|
IMcpServerListResponse,
|
||||||
IMCPTool,
|
IMCPTool,
|
||||||
} from '@/interfaces/database/mcp';
|
} from '@/interfaces/database/mcp';
|
||||||
import { ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
import {
|
||||||
|
IImportMcpServersRequestBody,
|
||||||
|
ITestMcpRequestBody,
|
||||||
|
} from '@/interfaces/request/mcp';
|
||||||
import i18n from '@/locales/config';
|
import i18n from '@/locales/config';
|
||||||
import mcpServerService from '@/services/mcp-server-service';
|
import mcpServerService from '@/services/mcp-server-service';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
@ -132,10 +137,10 @@ export const useImportMcpServer = () => {
|
|||||||
mutateAsync,
|
mutateAsync,
|
||||||
} = useMutation({
|
} = useMutation({
|
||||||
mutationKey: [McpApiAction.ImportMcpServer],
|
mutationKey: [McpApiAction.ImportMcpServer],
|
||||||
mutationFn: async (params: Record<string, any>) => {
|
mutationFn: async (params: IImportMcpServersRequestBody) => {
|
||||||
const { data = {} } = await mcpServerService.import(params);
|
const { data = {} } = await mcpServerService.import(params);
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
message.success(i18n.t(`message.created`));
|
message.success(i18n.t(`message.operated`));
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [McpApiAction.ListMcpServer],
|
queryKey: [McpApiAction.ListMcpServer],
|
||||||
@ -148,6 +153,25 @@ export const useImportMcpServer = () => {
|
|||||||
return { data, loading, importMcpServer: mutateAsync };
|
return { data, loading, importMcpServer: mutateAsync };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useExportMcpServer = () => {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isPending: loading,
|
||||||
|
mutateAsync,
|
||||||
|
} = useMutation<ResponseType<IExportedMcpServers>, Error, string[]>({
|
||||||
|
mutationKey: [McpApiAction.ExportMcpServer],
|
||||||
|
mutationFn: async (ids) => {
|
||||||
|
const { data = {} } = await mcpServerService.export({ mcp_ids: ids });
|
||||||
|
if (data.code === 0) {
|
||||||
|
message.success(i18n.t(`message.operated`));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data, loading, exportMcpServer: mutateAsync };
|
||||||
|
};
|
||||||
|
|
||||||
export const useListMcpServerTools = () => {
|
export const useListMcpServerTools = () => {
|
||||||
const { data, isFetching: loading } = useQuery({
|
const { data, isFetching: loading } = useQuery({
|
||||||
queryKey: [McpApiAction.ListMcpServerTools],
|
queryKey: [McpApiAction.ListMcpServerTools],
|
||||||
|
|||||||
@ -39,3 +39,20 @@ interface ISymbol {
|
|||||||
title: string;
|
title: string;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IExportedMcpServers {
|
||||||
|
mcpServers: McpServers;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpServers {
|
||||||
|
fetch_2: IExportedMcpServer;
|
||||||
|
github_1: IExportedMcpServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExportedMcpServer {
|
||||||
|
authorization_token: string;
|
||||||
|
name: string;
|
||||||
|
tool_configuration: Record<string, any>;
|
||||||
|
type: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { IExportedMcpServer } from '@/interfaces/database/mcp';
|
||||||
|
|
||||||
export interface ITestMcpRequestBody {
|
export interface ITestMcpRequestBody {
|
||||||
server_type: string;
|
server_type: string;
|
||||||
url: string;
|
url: string;
|
||||||
@ -5,3 +7,10 @@ export interface ITestMcpRequestBody {
|
|||||||
variables?: Record<string, any>;
|
variables?: Record<string, any>;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IImportMcpServersRequestBody {
|
||||||
|
mcpServers: Record<
|
||||||
|
string,
|
||||||
|
Pick<IExportedMcpServer, 'type' | 'url' | 'authorization_token'>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|||||||
@ -1303,5 +1303,10 @@ This delimiter is used to split the input text into several text pieces echo of
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mcp: {
|
||||||
|
export: 'Export',
|
||||||
|
import: 'Import',
|
||||||
|
addMcp: 'Add MCP',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -62,7 +62,6 @@ export function EditMcpDialog({
|
|||||||
? { name: '', server_type: ServerType.SSE, url: '' }
|
? { name: '', server_type: ServerType.SSE, url: '' }
|
||||||
: pick(data, ['name', 'server_type', 'url']),
|
: pick(data, ['name', 'server_type', 'url']),
|
||||||
});
|
});
|
||||||
console.log('🚀 ~ form:', form.formState.dirtyFields);
|
|
||||||
|
|
||||||
const handleTest: MouseEventHandler<HTMLButtonElement> = useCallback((e) => {
|
const handleTest: MouseEventHandler<HTMLButtonElement> = useCallback((e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@ -0,0 +1,74 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { FileUploader } from '@/components/file-uploader';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { FileMimeType, Platform } from '@/constants/common';
|
||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { TagRenameId } from '@/pages/add-knowledge/constant';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function ImportMcpForm({ hideModal, onOk }: IModalProps<any>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const FormSchema = z.object({
|
||||||
|
platform: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: t('common.namePlaceholder'),
|
||||||
|
})
|
||||||
|
.trim(),
|
||||||
|
fileList: z.array(z.instanceof(File)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
defaultValues: { platform: Platform.RAGFlow },
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||||
|
const ret = await onOk?.(data);
|
||||||
|
if (ret) {
|
||||||
|
hideModal?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-6"
|
||||||
|
id={TagRenameId}
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fileList"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('common.name')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<FileUploader
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
maxFileCount={1}
|
||||||
|
maxSize={4 * 1024 * 1024}
|
||||||
|
accept={{ '*.json': [FileMimeType.Json] }}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { LoadingButton } from '@/components/ui/loading-button';
|
||||||
|
import { IModalProps } from '@/interfaces/common';
|
||||||
|
import { TagRenameId } from '@/pages/add-knowledge/constant';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ImportMcpForm } from './import-mcp-form';
|
||||||
|
|
||||||
|
export function ImportMcpDialog({
|
||||||
|
hideModal,
|
||||||
|
onOk,
|
||||||
|
loading,
|
||||||
|
}: IModalProps<any>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open onOpenChange={hideModal}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('mcp.import')}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<ImportMcpForm hideModal={hideModal} onOk={onOk}></ImportMcpForm>
|
||||||
|
<DialogFooter>
|
||||||
|
<LoadingButton type="submit" form={TagRenameId} loading={loading}>
|
||||||
|
{t('common.save')}
|
||||||
|
</LoadingButton>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -3,29 +3,37 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { SearchInput } from '@/components/ui/input';
|
import { SearchInput } from '@/components/ui/input';
|
||||||
import { useListMcpServer } from '@/hooks/use-mcp-request';
|
import { useListMcpServer } from '@/hooks/use-mcp-request';
|
||||||
import { Import, Plus } from 'lucide-react';
|
import { Import, Plus } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { EditMcpDialog } from './edit-mcp-dialog';
|
import { EditMcpDialog } from './edit-mcp-dialog';
|
||||||
|
import { ImportMcpDialog } from './import-mcp-dialog';
|
||||||
import { McpCard } from './mcp-card';
|
import { McpCard } from './mcp-card';
|
||||||
import { useBulkOperateMCP } from './use-bulk-operate-mcp';
|
import { useBulkOperateMCP } from './use-bulk-operate-mcp';
|
||||||
import { useEditMcp } from './use-edit-mcp';
|
import { useEditMcp } from './use-edit-mcp';
|
||||||
|
import { useImportMcp } from './use-import-mcp';
|
||||||
|
|
||||||
export default function McpServer() {
|
export default function McpServer() {
|
||||||
const { data } = useListMcpServer();
|
const { data } = useListMcpServer();
|
||||||
const { editVisible, showEditModal, hideEditModal, handleOk, id } =
|
const { editVisible, showEditModal, hideEditModal, handleOk, id } =
|
||||||
useEditMcp();
|
useEditMcp();
|
||||||
const { list, selectedList, handleSelectChange } = useBulkOperateMCP();
|
const { list, selectedList, handleSelectChange } = useBulkOperateMCP();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { importVisible, showImportModal, hideImportModal, onImportOk } =
|
||||||
|
useImportMcp();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="p-4">
|
<section className="p-4">
|
||||||
<div className="text-text-title text-2xl">MCP Servers</div>
|
<div className="text-text-title text-2xl">MCP Servers</div>
|
||||||
<section className="flex items-center justify-between pb-5">
|
<section className="flex items-center justify-between pb-5">
|
||||||
<div className="text-text-sub-title">自定义 MCP Server 的列表</div>
|
<div className="text-text-sub-title">
|
||||||
|
Customize the list of MCP servers
|
||||||
|
</div>
|
||||||
<div className="flex gap-5">
|
<div className="flex gap-5">
|
||||||
<SearchInput className="w-40"></SearchInput>
|
<SearchInput className="w-40"></SearchInput>
|
||||||
<Button variant={'secondary'}>
|
<Button variant={'secondary'} onClick={showImportModal}>
|
||||||
<Import /> Import
|
<Import /> {t('mcp.import')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={showEditModal('')}>
|
<Button onClick={showEditModal('')}>
|
||||||
<Plus /> Add MCP
|
<Plus /> {t('mcp.addMcp')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -54,6 +62,12 @@ export default function McpServer() {
|
|||||||
id={id}
|
id={id}
|
||||||
></EditMcpDialog>
|
></EditMcpDialog>
|
||||||
)}
|
)}
|
||||||
|
{importVisible && (
|
||||||
|
<ImportMcpDialog
|
||||||
|
hideModal={hideImportModal}
|
||||||
|
onOk={onImportOk}
|
||||||
|
></ImportMcpDialog>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,10 +7,11 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useDeleteMcpServer } from '@/hooks/use-mcp-request';
|
import { useDeleteMcpServer } from '@/hooks/use-mcp-request';
|
||||||
import { PenLine, Trash2 } from 'lucide-react';
|
import { PenLine, Trash2, Upload } from 'lucide-react';
|
||||||
import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
|
import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { UseEditMcpReturnType } from './use-edit-mcp';
|
import { UseEditMcpReturnType } from './use-edit-mcp';
|
||||||
|
import { useExportMcp } from './use-export-mcp';
|
||||||
|
|
||||||
export function McpDropdown({
|
export function McpDropdown({
|
||||||
children,
|
children,
|
||||||
@ -22,6 +23,7 @@ export function McpDropdown({
|
|||||||
>) {
|
>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { deleteMcpServer } = useDeleteMcpServer();
|
const { deleteMcpServer } = useDeleteMcpServer();
|
||||||
|
const { handleExportMcpJson } = useExportMcp();
|
||||||
|
|
||||||
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
|
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
|
||||||
deleteMcpServer([mcpId]);
|
deleteMcpServer([mcpId]);
|
||||||
@ -35,6 +37,10 @@ export function McpDropdown({
|
|||||||
{t('common.edit')} <PenLine />
|
{t('common.edit')} <PenLine />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={handleExportMcpJson([mcpId])}>
|
||||||
|
{t('mcp.export')} <Upload />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<ConfirmDeleteDialog onOk={handleDelete}>
|
<ConfirmDeleteDialog onOk={handleDelete}>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-text-delete-red"
|
className="text-text-delete-red"
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import { useDeleteMcpServer } from '@/hooks/use-mcp-request';
|
|||||||
import { Trash2, Upload } from 'lucide-react';
|
import { Trash2, Upload } from 'lucide-react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useExportMcp } from './use-export-mcp';
|
||||||
|
|
||||||
export function useBulkOperateMCP() {
|
export function useBulkOperateMCP() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedList, setSelectedList] = useState<Array<string>>([]);
|
const [selectedList, setSelectedList] = useState<Array<string>>([]);
|
||||||
const { deleteMcpServer } = useDeleteMcpServer();
|
const { deleteMcpServer } = useDeleteMcpServer();
|
||||||
|
const { handleExportMcpJson } = useExportMcp();
|
||||||
const handleEnableClick = useCallback(() => {}, []);
|
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
deleteMcpServer(selectedList);
|
deleteMcpServer(selectedList);
|
||||||
@ -25,7 +25,7 @@ export function useBulkOperateMCP() {
|
|||||||
id: 'export',
|
id: 'export',
|
||||||
label: t('mcp.export'),
|
label: t('mcp.export'),
|
||||||
icon: <Upload />,
|
icon: <Upload />,
|
||||||
onClick: handleEnableClick,
|
onClick: handleExportMcpJson(selectedList),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'delete',
|
id: 'delete',
|
||||||
|
|||||||
21
web/src/pages/profile-setting/mcp/use-export-mcp.ts
Normal file
21
web/src/pages/profile-setting/mcp/use-export-mcp.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useExportMcpServer } from '@/hooks/use-mcp-request';
|
||||||
|
import { downloadJsonFile } from '@/utils/file-util';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export function useExportMcp() {
|
||||||
|
const { exportMcpServer } = useExportMcpServer();
|
||||||
|
|
||||||
|
const handleExportMcpJson = useCallback(
|
||||||
|
(ids: string[]) => async () => {
|
||||||
|
const data = await exportMcpServer(ids);
|
||||||
|
if (data.code === 0) {
|
||||||
|
downloadJsonFile(data.data, `mcp.json`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[exportMcpServer],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleExportMcpJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
73
web/src/pages/profile-setting/mcp/use-import-mcp.ts
Normal file
73
web/src/pages/profile-setting/mcp/use-import-mcp.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import message from '@/components/ui/message';
|
||||||
|
import { FileMimeType } from '@/constants/common';
|
||||||
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
|
import { useImportMcpServer } from '@/hooks/use-mcp-request';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const ServerEntrySchema = z.object({
|
||||||
|
authorization_token: z.string().optional(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
tool_configuration: z.object({}).passthrough().optional(),
|
||||||
|
type: z.string(),
|
||||||
|
url: z.string().url(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const McpConfigSchema = z.object({
|
||||||
|
mcpServers: z.record(ServerEntrySchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useImportMcp = () => {
|
||||||
|
const {
|
||||||
|
visible: importVisible,
|
||||||
|
hideModal: hideImportModal,
|
||||||
|
showModal: showImportModal,
|
||||||
|
} = useSetModalState();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { importMcpServer, loading } = useImportMcpServer();
|
||||||
|
|
||||||
|
const onImportOk = useCallback(
|
||||||
|
async ({ fileList }: { fileList: File[] }) => {
|
||||||
|
if (fileList.length > 0) {
|
||||||
|
const file = fileList[0];
|
||||||
|
if (file.type !== FileMimeType.Json) {
|
||||||
|
message.error(t('flow.jsonUploadTypeErrorMessage'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcpStr = await file.text();
|
||||||
|
const errorMessage = t('flow.jsonUploadContentErrorMessage');
|
||||||
|
try {
|
||||||
|
const mcp = JSON.parse(mcpStr);
|
||||||
|
try {
|
||||||
|
McpConfigSchema.parse(mcp);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Incorrect data format');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mcpStr && !isEmpty(mcp)) {
|
||||||
|
const ret = await importMcpServer(mcp);
|
||||||
|
if (ret.code === 0) {
|
||||||
|
hideImportModal();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hideImportModal, importMcpServer, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
importVisible,
|
||||||
|
showImportModal,
|
||||||
|
hideImportModal,
|
||||||
|
onImportOk,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -7,7 +7,7 @@ export const isFormData = (data: unknown): data is FormData => {
|
|||||||
return data instanceof FormData;
|
return data instanceof FormData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const excludedFields = ['img2txt_id'];
|
const excludedFields = ['img2txt_id', 'mcpServers'];
|
||||||
|
|
||||||
const isExcludedField = (key: string) => {
|
const isExcludedField = (key: string) => {
|
||||||
return excludedFields.includes(key);
|
return excludedFields.includes(key);
|
||||||
|
|||||||
Reference in New Issue
Block a user