mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Feat: Edit MCP server #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -3,17 +3,31 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible';
|
} from '@/components/ui/collapsible';
|
||||||
|
import { CollapsibleProps } from '@radix-ui/react-collapsible';
|
||||||
import { ListCollapse } from 'lucide-react';
|
import { ListCollapse } from 'lucide-react';
|
||||||
import { PropsWithChildren, ReactNode } from 'react';
|
import { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
type CollapseProps = {
|
type CollapseProps = Omit<CollapsibleProps, 'title'> & {
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
rightContent?: ReactNode;
|
rightContent?: ReactNode;
|
||||||
} & PropsWithChildren;
|
} & PropsWithChildren;
|
||||||
|
|
||||||
export function Collapse({ title, children, rightContent }: CollapseProps) {
|
export function Collapse({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
rightContent,
|
||||||
|
open,
|
||||||
|
defaultOpen = true,
|
||||||
|
onOpenChange,
|
||||||
|
disabled,
|
||||||
|
}: CollapseProps) {
|
||||||
return (
|
return (
|
||||||
<Collapsible defaultOpen>
|
<Collapsible
|
||||||
|
defaultOpen={defaultOpen}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
<CollapsibleTrigger className="w-full">
|
<CollapsibleTrigger className="w-full">
|
||||||
<section className="flex justify-between items-center pb-2">
|
<section className="flex justify-between items-center pb-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import message from '@/components/ui/message';
|
import message from '@/components/ui/message';
|
||||||
import { IMcpServerListResponse, IMCPTool } from '@/interfaces/database/mcp';
|
import {
|
||||||
|
IMcpServer,
|
||||||
|
IMcpServerListResponse,
|
||||||
|
IMCPTool,
|
||||||
|
} from '@/interfaces/database/mcp';
|
||||||
import { ITestMcpRequestBody } from '@/interfaces/request/mcp';
|
import { 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';
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
export const enum McpApiAction {
|
export const enum McpApiAction {
|
||||||
ListMcpServer = 'listMcpServer',
|
ListMcpServer = 'listMcpServer',
|
||||||
@ -34,20 +37,19 @@ export const useListMcpServer = () => {
|
|||||||
return { data, loading };
|
return { data, loading };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetMcpServer = () => {
|
export const useGetMcpServer = (id: string) => {
|
||||||
const [id, setId] = useState('');
|
const { data, isFetching: loading } = useQuery<IMcpServer>({
|
||||||
const { data, isFetching: loading } = useQuery({
|
|
||||||
queryKey: [McpApiAction.GetMcpServer, id],
|
queryKey: [McpApiAction.GetMcpServer, id],
|
||||||
initialData: {},
|
initialData: {} as IMcpServer,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await mcpServerService.get();
|
const { data } = await mcpServerService.get({ mcp_id: id });
|
||||||
return data?.data ?? {};
|
return data?.data ?? {};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data, loading, setId, id };
|
return { data, loading, id };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCreateMcpServer = () => {
|
export const useCreateMcpServer = () => {
|
||||||
|
|||||||
@ -7,15 +7,22 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { useTestMcpServer } from '@/hooks/use-mcp-request';
|
import { useGetMcpServer, useTestMcpServer } from '@/hooks/use-mcp-request';
|
||||||
import { IModalProps } from '@/interfaces/common';
|
import { IModalProps } from '@/interfaces/common';
|
||||||
import { IMCPTool, IMCPToolObject } from '@/interfaces/database/mcp';
|
import { IMCPTool, IMCPToolObject } from '@/interfaces/database/mcp';
|
||||||
import { omit } from 'lodash';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { isEmpty, omit, pick } from 'lodash';
|
||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw } from 'lucide-react';
|
||||||
import { MouseEventHandler, useCallback, useState } from 'react';
|
import { MouseEventHandler, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { EditMcpForm, FormId, useBuildFormSchema } from './edit-mcp-form';
|
import {
|
||||||
|
EditMcpForm,
|
||||||
|
FormId,
|
||||||
|
ServerType,
|
||||||
|
useBuildFormSchema,
|
||||||
|
} from './edit-mcp-form';
|
||||||
import { McpToolCard } from './tool-card';
|
import { McpToolCard } from './tool-card';
|
||||||
|
|
||||||
function transferToolToObject(tools: IMCPTool[] = []) {
|
function transferToolToObject(tools: IMCPTool[] = []) {
|
||||||
@ -25,11 +32,37 @@ function transferToolToObject(tools: IMCPTool[] = []) {
|
|||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditMcpDialog({ hideModal, loading, onOk }: IModalProps<any>) {
|
function transferToolToArray(tools: IMCPToolObject) {
|
||||||
|
return Object.entries(tools).reduce<IMCPTool[]>((pre, [name, tool]) => {
|
||||||
|
pre.push({ ...tool, name });
|
||||||
|
return pre;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditMcpDialog({
|
||||||
|
hideModal,
|
||||||
|
loading,
|
||||||
|
onOk,
|
||||||
|
id,
|
||||||
|
}: IModalProps<any> & { id: string }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { testMcpServer, data: tools } = useTestMcpServer();
|
const {
|
||||||
|
testMcpServer,
|
||||||
|
data: tools,
|
||||||
|
loading: testLoading,
|
||||||
|
} = useTestMcpServer();
|
||||||
const [isTriggeredBySaving, setIsTriggeredBySaving] = useState(false);
|
const [isTriggeredBySaving, setIsTriggeredBySaving] = useState(false);
|
||||||
const FormSchema = useBuildFormSchema();
|
const FormSchema = useBuildFormSchema();
|
||||||
|
const [collapseOpen, setCollapseOpen] = useState(true);
|
||||||
|
const { data } = useGetMcpServer(id);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
values: isEmpty(data)
|
||||||
|
? { name: '', server_type: ServerType.SSE, 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();
|
||||||
@ -54,15 +87,25 @@ export function EditMcpDialog({ hideModal, loading, onOk }: IModalProps<any>) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nextTools = useMemo(() => {
|
||||||
|
return tools || transferToolToArray(data.variables?.tools || {});
|
||||||
|
}, [data.variables?.tools, tools]);
|
||||||
|
|
||||||
|
const dirtyFields = form.formState.dirtyFields;
|
||||||
|
const fieldChanged = 'server_type' in dirtyFields || 'url' in dirtyFields;
|
||||||
|
const disabled = !!!tools?.length || testLoading || fieldChanged;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onOpenChange={hideModal}>
|
<Dialog open onOpenChange={hideModal}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Edit profile</DialogTitle>
|
<DialogTitle>Edit profile</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<EditMcpForm onOk={handleOk}></EditMcpForm>
|
<EditMcpForm onOk={handleOk} form={form}></EditMcpForm>
|
||||||
<Collapse
|
<Collapse
|
||||||
title={<div>{tools?.length || 0} tools available</div>}
|
title={<div>{tools?.length || 0} tools available</div>}
|
||||||
|
open={collapseOpen}
|
||||||
|
onOpenChange={setCollapseOpen}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Button
|
<Button
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
@ -74,8 +117,8 @@ export function EditMcpDialog({ hideModal, loading, onOk }: IModalProps<any>) {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5 overflow-auto max-h-80">
|
||||||
{tools?.map((x) => (
|
{nextTools?.map((x) => (
|
||||||
<McpToolCard key={x.name} data={x}></McpToolCard>
|
<McpToolCard key={x.name} data={x}></McpToolCard>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -86,7 +129,7 @@ export function EditMcpDialog({ hideModal, loading, onOk }: IModalProps<any>) {
|
|||||||
form={FormId}
|
form={FormId}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={!!!tools?.length}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{t('common.save')}
|
{t('common.save')}
|
||||||
</ButtonLoading>
|
</ButtonLoading>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { UseFormReturn } from 'react-hook-form';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -16,12 +15,11 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { RAGFlowSelect } from '@/components/ui/select';
|
import { RAGFlowSelect } from '@/components/ui/select';
|
||||||
import { IModalProps } from '@/interfaces/common';
|
import { IModalProps } from '@/interfaces/common';
|
||||||
import { buildOptions } from '@/utils/form';
|
import { buildOptions } from '@/utils/form';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export const FormId = 'EditMcpForm';
|
export const FormId = 'EditMcpForm';
|
||||||
|
|
||||||
enum ServerType {
|
export enum ServerType {
|
||||||
SSE = 'sse',
|
SSE = 'sse',
|
||||||
StreamableHttp = 'streamable-http',
|
StreamableHttp = 'streamable-http',
|
||||||
}
|
}
|
||||||
@ -57,28 +55,16 @@ export function useBuildFormSchema() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EditMcpForm({
|
export function EditMcpForm({
|
||||||
initialName,
|
form,
|
||||||
onOk,
|
onOk,
|
||||||
}: IModalProps<any> & { initialName?: string }) {
|
}: IModalProps<any> & { form: UseFormReturn<any> }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const FormSchema = useBuildFormSchema();
|
const FormSchema = useBuildFormSchema();
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof FormSchema>>({
|
|
||||||
resolver: zodResolver(FormSchema),
|
|
||||||
defaultValues: { name: '', server_type: ServerType.SSE, url: '' },
|
|
||||||
});
|
|
||||||
|
|
||||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||||
onOk?.(data);
|
onOk?.(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialName) {
|
|
||||||
form.setValue('name', initialName);
|
|
||||||
}
|
|
||||||
}, [form, initialName]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
@ -10,7 +10,8 @@ import { useEditMcp } from './use-edit-mcp';
|
|||||||
|
|
||||||
export default function McpServer() {
|
export default function McpServer() {
|
||||||
const { data } = useListMcpServer();
|
const { data } = useListMcpServer();
|
||||||
const { editVisible, showEditModal, hideEditModal, handleOk } = useEditMcp();
|
const { editVisible, showEditModal, hideEditModal, handleOk, id } =
|
||||||
|
useEditMcp();
|
||||||
const { list, selectedList, handleSelectChange } = useBulkOperateMCP();
|
const { list, selectedList, handleSelectChange } = useBulkOperateMCP();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -42,6 +43,7 @@ export default function McpServer() {
|
|||||||
data={item}
|
data={item}
|
||||||
selectedList={selectedList}
|
selectedList={selectedList}
|
||||||
handleSelectChange={handleSelectChange}
|
handleSelectChange={handleSelectChange}
|
||||||
|
showEditModal={showEditModal}
|
||||||
></McpCard>
|
></McpCard>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
@ -49,6 +51,7 @@ export default function McpServer() {
|
|||||||
<EditMcpDialog
|
<EditMcpDialog
|
||||||
hideModal={hideEditModal}
|
hideModal={hideEditModal}
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
|
id={id}
|
||||||
></EditMcpDialog>
|
></EditMcpDialog>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -7,15 +7,18 @@ import { isPlainObject } from 'lodash';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { McpDropdown } from './mcp-dropdown';
|
import { McpDropdown } from './mcp-dropdown';
|
||||||
import { UseBulkOperateMCPReturnType } from './use-bulk-operate-mcp';
|
import { UseBulkOperateMCPReturnType } from './use-bulk-operate-mcp';
|
||||||
|
import { UseEditMcpReturnType } from './use-edit-mcp';
|
||||||
|
|
||||||
export type DatasetCardProps = {
|
export type DatasetCardProps = {
|
||||||
data: IMcpServer;
|
data: IMcpServer;
|
||||||
} & Pick<UseBulkOperateMCPReturnType, 'handleSelectChange' | 'selectedList'>;
|
} & Pick<UseBulkOperateMCPReturnType, 'handleSelectChange' | 'selectedList'> &
|
||||||
|
Pick<UseEditMcpReturnType, 'showEditModal'>;
|
||||||
|
|
||||||
export function McpCard({
|
export function McpCard({
|
||||||
data,
|
data,
|
||||||
selectedList,
|
selectedList,
|
||||||
handleSelectChange,
|
handleSelectChange,
|
||||||
|
showEditModal,
|
||||||
}: DatasetCardProps) {
|
}: DatasetCardProps) {
|
||||||
const toolLength = useMemo(() => {
|
const toolLength = useMemo(() => {
|
||||||
const tools = data.variables?.tools;
|
const tools = data.variables?.tools;
|
||||||
@ -35,7 +38,7 @@ export function McpCard({
|
|||||||
<section className="flex justify-between pb-2">
|
<section className="flex justify-between pb-2">
|
||||||
<h3 className="text-lg font-semibold line-clamp-1">{data.name}</h3>
|
<h3 className="text-lg font-semibold line-clamp-1">{data.name}</h3>
|
||||||
<div className="space-x-4">
|
<div className="space-x-4">
|
||||||
<McpDropdown mcpId={data.id}>
|
<McpDropdown mcpId={data.id} showEditModal={showEditModal}>
|
||||||
<MoreButton></MoreButton>
|
<MoreButton></MoreButton>
|
||||||
</McpDropdown>
|
</McpDropdown>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@ -10,19 +10,19 @@ import { useDeleteMcpServer } from '@/hooks/use-mcp-request';
|
|||||||
import { PenLine, Trash2 } from 'lucide-react';
|
import { PenLine, Trash2 } 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';
|
||||||
|
|
||||||
export function McpDropdown({
|
export function McpDropdown({
|
||||||
children,
|
children,
|
||||||
mcpId,
|
mcpId,
|
||||||
}: PropsWithChildren & { mcpId: string }) {
|
showEditModal,
|
||||||
|
}: PropsWithChildren & { mcpId: string } & Pick<
|
||||||
|
UseEditMcpReturnType,
|
||||||
|
'showEditModal'
|
||||||
|
>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { deleteMcpServer } = useDeleteMcpServer();
|
const { deleteMcpServer } = useDeleteMcpServer();
|
||||||
|
|
||||||
const handleShowAgentRenameModal: MouseEventHandler<HTMLDivElement> =
|
|
||||||
useCallback((e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
|
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
|
||||||
deleteMcpServer([mcpId]);
|
deleteMcpServer([mcpId]);
|
||||||
}, [deleteMcpServer, mcpId]);
|
}, [deleteMcpServer, mcpId]);
|
||||||
@ -31,7 +31,7 @@ export function McpDropdown({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem onClick={handleShowAgentRenameModal}>
|
<DropdownMenuItem onClick={showEditModal(mcpId)}>
|
||||||
{t('common.edit')} <PenLine />
|
{t('common.edit')} <PenLine />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { useSetModalState } from '@/hooks/common-hooks';
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
import {
|
import {
|
||||||
useCreateMcpServer,
|
useCreateMcpServer,
|
||||||
useGetMcpServer,
|
|
||||||
useUpdateMcpServer,
|
useUpdateMcpServer,
|
||||||
} from '@/hooks/use-mcp-request';
|
} from '@/hooks/use-mcp-request';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
export const useEditMcp = () => {
|
export const useEditMcp = () => {
|
||||||
const {
|
const {
|
||||||
@ -13,14 +12,13 @@ export const useEditMcp = () => {
|
|||||||
showModal: showEditModal,
|
showModal: showEditModal,
|
||||||
} = useSetModalState();
|
} = useSetModalState();
|
||||||
const { createMcpServer, loading } = useCreateMcpServer();
|
const { createMcpServer, loading } = useCreateMcpServer();
|
||||||
const { data, setId, id } = useGetMcpServer();
|
const [id, setId] = useState('');
|
||||||
|
|
||||||
const { updateMcpServer } = useUpdateMcpServer();
|
const { updateMcpServer } = useUpdateMcpServer();
|
||||||
|
|
||||||
const handleShowModal = useCallback(
|
const handleShowModal = useCallback(
|
||||||
(id?: string) => () => {
|
(id: string) => () => {
|
||||||
if (id) {
|
setId(id);
|
||||||
setId(id);
|
|
||||||
}
|
|
||||||
showEditModal();
|
showEditModal();
|
||||||
},
|
},
|
||||||
[setId, showEditModal],
|
[setId, showEditModal],
|
||||||
@ -47,7 +45,9 @@ export const useEditMcp = () => {
|
|||||||
showEditModal: handleShowModal,
|
showEditModal: handleShowModal,
|
||||||
loading,
|
loading,
|
||||||
createMcpServer,
|
createMcpServer,
|
||||||
detail: data,
|
|
||||||
handleOk,
|
handleOk,
|
||||||
|
id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UseEditMcpReturnType = ReturnType<typeof useEditMcp>;
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const methods = {
|
|||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
url: getMcpServer,
|
url: getMcpServer,
|
||||||
method: 'post',
|
method: 'get',
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
url: createMcpServer,
|
url: createMcpServer,
|
||||||
|
|||||||
Reference in New Issue
Block a user