mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-30 15:18:50 +01:00
make MCPFormPanel agnostic to Agent / Chat context
This commit is contained in:
parent
cf91dc3aad
commit
389ab1db77
6 changed files with 233 additions and 152 deletions
112
client/src/components/SidePanel/Agents/AgentMCPFormPanel.tsx
Normal file
112
client/src/components/SidePanel/Agents/AgentMCPFormPanel.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { Panel } from '~/common';
|
||||
import type { MCP } from '~/common';
|
||||
import MCPFormPanel from '../MCP/MCPFormPanel';
|
||||
|
||||
// TODO: Add MCP delete (for now mocked for ui)
|
||||
// import { useDeleteAgentMCP } from '~/data-provider';
|
||||
|
||||
function useDeleteAgentMCP({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess: () => void;
|
||||
onError: (error: Error) => void;
|
||||
}) {
|
||||
return {
|
||||
mutate: async ({ mcp_id, agent_id }: { mcp_id: string; agent_id: string }) => {
|
||||
try {
|
||||
console.log('Mock delete MCP:', { mcp_id, agent_id });
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
onError(error as Error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function useUpdateAgentMCP({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess: (mcp: MCP) => void;
|
||||
onError: (error: Error) => void;
|
||||
}) {
|
||||
return {
|
||||
mutate: async (mcp: MCP) => {
|
||||
try {
|
||||
// TODO: Implement MCP endpoint
|
||||
onSuccess(mcp);
|
||||
} catch (error) {
|
||||
onError(error as Error);
|
||||
}
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
export default function AgentMCPFormPanel() {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const { mcp, setMcp, agent_id, setActivePanel } = useAgentPanelContext();
|
||||
|
||||
const updateAgentMCP = useUpdateAgentMCP({
|
||||
onSuccess(mcp) {
|
||||
showToast({
|
||||
message: localize('com_ui_update_mcp_success'),
|
||||
status: 'success',
|
||||
});
|
||||
setMcp(mcp);
|
||||
},
|
||||
onError(error) {
|
||||
showToast({
|
||||
message: (error as Error).message || localize('com_ui_update_mcp_error'),
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const deleteAgentMCP = useDeleteAgentMCP({
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_delete_mcp_success'),
|
||||
status: 'success',
|
||||
});
|
||||
setActivePanel(Panel.builder);
|
||||
setMcp(undefined);
|
||||
},
|
||||
onError(error) {
|
||||
showToast({
|
||||
message: (error as Error).message ?? localize('com_ui_delete_mcp_error'),
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleBack = () => {
|
||||
setActivePanel(Panel.builder);
|
||||
setMcp(undefined);
|
||||
};
|
||||
|
||||
const handleSave = (mcp: MCP) => {
|
||||
updateAgentMCP.mutate(mcp);
|
||||
};
|
||||
|
||||
const handleDelete = (mcp_id: string, contextId: string) => {
|
||||
deleteAgentMCP.mutate({ mcp_id, agent_id: contextId });
|
||||
};
|
||||
|
||||
return (
|
||||
<MCPFormPanel
|
||||
mcp={mcp}
|
||||
contextId={agent_id}
|
||||
onBack={handleBack}
|
||||
onDelete={handleDelete}
|
||||
onSave={handleSave}
|
||||
showDeleteButton={!!mcp}
|
||||
isDeleteDisabled={!agent_id || !mcp?.mcp_id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import VersionPanel from './Version/VersionPanel';
|
|||
import { useChatContext } from '~/Providers';
|
||||
import ActionsPanel from './ActionsPanel';
|
||||
import AgentPanel from './AgentPanel';
|
||||
import MCPPanel from './MCPPanel';
|
||||
import AgentMCPFormPanel from './AgentMCPFormPanel';
|
||||
import { Panel } from '~/common';
|
||||
|
||||
export default function AgentPanelSwitch() {
|
||||
|
|
@ -55,7 +55,7 @@ function AgentPanelSwitchWithContext() {
|
|||
return <VersionPanel />;
|
||||
}
|
||||
if (activePanel === Panel.mcp) {
|
||||
return <MCPPanel />;
|
||||
return <AgentMCPFormPanel />;
|
||||
}
|
||||
return <AgentPanel agentsConfig={agentsConfig} endpointsConfig={endpointsConfig} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,57 @@
|
|||
import { useEffect } from 'react';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { useAgentPanelContext } from '~/Providers/AgentPanelContext';
|
||||
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { defaultMCPFormValues } from '~/common/mcp';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { TrashIcon } from '~/components/svg';
|
||||
import type { MCPForm } from '~/common';
|
||||
import type { MCPForm, MCP } from '~/common';
|
||||
import MCPInput from './MCPInput';
|
||||
import { Panel } from '~/common';
|
||||
import {
|
||||
AuthTypeEnum,
|
||||
AuthorizationTypeEnum,
|
||||
TokenExchangeMethodEnum,
|
||||
} from 'librechat-data-provider';
|
||||
// TODO: Add MCP delete (for now mocked for ui)
|
||||
// import { useDeleteAgentMCP } from '~/data-provider';
|
||||
|
||||
function useDeleteAgentMCP({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess: () => void;
|
||||
onError: (error: Error) => void;
|
||||
}) {
|
||||
return {
|
||||
mutate: async ({ mcp_id, agent_id }: { mcp_id: string; agent_id: string }) => {
|
||||
try {
|
||||
console.log('Mock delete MCP:', { mcp_id, agent_id });
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
onError(error as Error);
|
||||
}
|
||||
},
|
||||
};
|
||||
interface MCPFormPanelProps {
|
||||
// Data
|
||||
mcp?: MCP;
|
||||
agent_id?: string; // agent_id, conversation_id, etc.
|
||||
|
||||
// Actions
|
||||
onBack: () => void;
|
||||
onDelete?: (mcp_id: string, agent_id: string) => void;
|
||||
onSave: (mcp: MCP) => void;
|
||||
|
||||
// UI customization
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
showDeleteButton?: boolean;
|
||||
isDeleteDisabled?: boolean;
|
||||
deleteConfirmMessage?: string;
|
||||
|
||||
// Form customization
|
||||
defaultValues?: Partial<MCPForm>;
|
||||
}
|
||||
|
||||
export default function MCPPanel() {
|
||||
export default function MCPFormPanel({
|
||||
mcp,
|
||||
agent_id,
|
||||
onBack,
|
||||
onDelete,
|
||||
onSave,
|
||||
title,
|
||||
subtitle,
|
||||
showDeleteButton = true,
|
||||
isDeleteDisabled = false,
|
||||
deleteConfirmMessage,
|
||||
defaultValues = defaultMCPFormValues,
|
||||
}: MCPFormPanelProps) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const { mcp, setMcp, agent_id, setActivePanel } = useAgentPanelContext();
|
||||
const deleteAgentMCP = useDeleteAgentMCP({
|
||||
onSuccess: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_delete_mcp_success'),
|
||||
status: 'success',
|
||||
});
|
||||
setActivePanel(Panel.builder);
|
||||
setMcp(undefined);
|
||||
},
|
||||
onError(error) {
|
||||
showToast({
|
||||
message: (error as Error).message ?? localize('com_ui_delete_mcp_error'),
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const methods = useForm<MCPForm>({
|
||||
defaultValues: defaultMCPFormValues,
|
||||
defaultValues: defaultValues,
|
||||
});
|
||||
|
||||
const { reset } = methods;
|
||||
|
|
@ -96,33 +87,32 @@ export default function MCPPanel() {
|
|||
}
|
||||
}, [mcp, reset]);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (onDelete && mcp?.mcp_id && agent_id) {
|
||||
onDelete(mcp.mcp_id, agent_id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className="h-full grow overflow-hidden">
|
||||
<div className="h-full overflow-auto px-2 pb-12 text-sm">
|
||||
<div className="relative flex flex-col items-center px-16 py-6 text-center">
|
||||
<div className="absolute left-0 top-6">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-neutral relative"
|
||||
onClick={() => {
|
||||
setActivePanel(Panel.builder);
|
||||
setMcp(undefined);
|
||||
}}
|
||||
>
|
||||
<button type="button" className="btn btn-neutral relative" onClick={onBack}>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<ChevronLeft />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!!mcp && (
|
||||
{!!mcp && showDeleteButton && onDelete && (
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<div className="absolute right-0 top-6">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!agent_id || !mcp.mcp_id}
|
||||
disabled={isDeleteDisabled || !mcp.mcp_id || !agent_id}
|
||||
className="btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium"
|
||||
>
|
||||
<TrashIcon className="text-red-500" />
|
||||
|
|
@ -135,22 +125,11 @@ export default function MCPPanel() {
|
|||
className="max-w-[450px]"
|
||||
main={
|
||||
<Label className="text-left text-sm font-medium">
|
||||
{localize('com_ui_delete_mcp_confirm')}
|
||||
{deleteConfirmMessage || localize('com_ui_delete_mcp_confirm')}
|
||||
</Label>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: () => {
|
||||
if (!agent_id) {
|
||||
return showToast({
|
||||
message: localize('com_agents_no_agent_id_error'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
deleteAgentMCP.mutate({
|
||||
mcp_id: mcp.mcp_id,
|
||||
agent_id,
|
||||
});
|
||||
},
|
||||
selectHandler: handleDelete,
|
||||
selectClasses:
|
||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
|
|
@ -160,11 +139,14 @@ export default function MCPPanel() {
|
|||
)}
|
||||
|
||||
<div className="text-xl font-medium">
|
||||
{mcp ? localize('com_ui_edit_mcp_server') : localize('com_ui_add_mcp_server')}
|
||||
{title ||
|
||||
(mcp ? localize('com_ui_edit_mcp_server') : localize('com_ui_add_mcp_server'))}
|
||||
</div>
|
||||
<div className="text-xs text-text-secondary">
|
||||
{subtitle || localize('com_agents_mcp_info')}
|
||||
</div>
|
||||
<div className="text-xs text-text-secondary">{localize('com_agents_mcp_info')}</div>
|
||||
</div>
|
||||
<MCPInput mcp={mcp} agent_id={agent_id} setMCP={setMcp} />
|
||||
<MCPInput mcp={mcp} agent_id={agent_id} onSave={onSave} />
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
|
@ -5,54 +5,24 @@ import MCPAuth from '~/components/SidePanel/Builder/MCPAuth';
|
|||
import MCPIcon from '~/components/SidePanel/Agents/MCPIcon';
|
||||
import { Label, Checkbox } from '~/components/ui';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import { MCPForm } from '~/common/types';
|
||||
|
||||
function useUpdateAgentMCP({
|
||||
onSuccess,
|
||||
onError,
|
||||
}: {
|
||||
onSuccess: (data: [string, MCP]) => void;
|
||||
onError: (error: Error) => void;
|
||||
}) {
|
||||
return {
|
||||
mutate: async ({
|
||||
mcp_id,
|
||||
metadata,
|
||||
agent_id,
|
||||
}: {
|
||||
mcp_id?: string;
|
||||
metadata: MCP['metadata'];
|
||||
agent_id: string;
|
||||
}) => {
|
||||
try {
|
||||
// TODO: Implement MCP endpoint
|
||||
onSuccess(['success', { mcp_id, metadata, agent_id } as MCP]);
|
||||
} catch (error) {
|
||||
onError(error as Error);
|
||||
}
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
interface MCPInputProps {
|
||||
mcp?: MCP;
|
||||
agent_id?: string;
|
||||
setMCP: React.Dispatch<React.SetStateAction<MCP | undefined>>;
|
||||
onSave: (mcp: MCP) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
|
||||
export default function MCPInput({ mcp, a, onSave, isLoading = false }: MCPInputProps) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors },
|
||||
control,
|
||||
} = useFormContext<MCPForm>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showTools, setShowTools] = useState(false);
|
||||
const [selectedTools, setSelectedTools] = useState<string[]>([]);
|
||||
|
||||
|
|
@ -64,50 +34,16 @@ export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
|
|||
}
|
||||
}, [mcp]);
|
||||
|
||||
const updateAgentMCP = useUpdateAgentMCP({
|
||||
onSuccess(data) {
|
||||
showToast({
|
||||
message: localize('com_ui_update_mcp_success'),
|
||||
status: 'success',
|
||||
});
|
||||
setMCP(data[1]);
|
||||
setShowTools(true);
|
||||
setSelectedTools(data[1].metadata.tools ?? []);
|
||||
setIsLoading(false);
|
||||
},
|
||||
onError(error) {
|
||||
showToast({
|
||||
message: (error as Error).message || localize('com_ui_update_mcp_error'),
|
||||
status: 'error',
|
||||
});
|
||||
setIsLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const saveMCP = handleSubmit(async (data: MCPForm) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await updateAgentMCP.mutate({
|
||||
agent_id: agent_id ?? '',
|
||||
mcp_id: mcp?.mcp_id,
|
||||
metadata: {
|
||||
...data,
|
||||
tools: selectedTools,
|
||||
},
|
||||
});
|
||||
setMCP(response[1]);
|
||||
showToast({
|
||||
message: localize('com_ui_update_mcp_success'),
|
||||
status: 'success',
|
||||
});
|
||||
} catch {
|
||||
showToast({
|
||||
message: localize('com_ui_update_mcp_error'),
|
||||
status: 'error',
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
const updatedMCP: MCP = {
|
||||
mcp_id: mcp?.mcp_id ?? '',
|
||||
agent_id: a ?? '', // This will be agent_id, conversation_id, etc.
|
||||
metadata: {
|
||||
...data,
|
||||
tools: selectedTools,
|
||||
},
|
||||
};
|
||||
onSave(updatedMCP);
|
||||
});
|
||||
|
||||
const handleSelectAll = () => {
|
||||
|
|
@ -140,14 +76,15 @@ export default function MCPInput({ mcp, agent_id, setMCP }: MCPInputProps) {
|
|||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64String = reader.result as string;
|
||||
setMCP({
|
||||
const updatedMCP: MCP = {
|
||||
mcp_id: mcp?.mcp_id ?? '',
|
||||
agent_id: agent_id ?? '',
|
||||
agent_id: a ?? '',
|
||||
metadata: {
|
||||
...mcp?.metadata,
|
||||
icon: base64String,
|
||||
},
|
||||
});
|
||||
};
|
||||
onSave(updatedMCP);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ import { useGetStartupConfig } from '~/data-provider';
|
|||
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import MCPFormPanel from './MCPFormPanel';
|
||||
import type { MCP } from '~/common';
|
||||
|
||||
interface ServerConfigWithVars {
|
||||
serverName: string;
|
||||
|
|
@ -24,6 +26,7 @@ export default function MCPPanel() {
|
|||
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [showMCPForm, setShowMCPForm] = useState(false);
|
||||
|
||||
const mcpServerDefinitions = useMemo(() => {
|
||||
if (!startupConfig?.mcpServers) {
|
||||
|
|
@ -89,14 +92,54 @@ export default function MCPPanel() {
|
|||
setSelectedServerNameForEditing(null);
|
||||
};
|
||||
|
||||
const handleAddMCP = () => {
|
||||
setShowMCPForm(true);
|
||||
};
|
||||
|
||||
const handleBackFromForm = () => {
|
||||
setShowMCPForm(false);
|
||||
};
|
||||
|
||||
const handleSaveMCP = (mcp: MCP) => {
|
||||
// TODO: Implement MCP save logic for conversation context
|
||||
console.log('Saving MCP:', mcp);
|
||||
showToast({
|
||||
message: localize('com_ui_update_mcp_success'),
|
||||
status: 'success',
|
||||
});
|
||||
setShowMCPForm(false);
|
||||
};
|
||||
|
||||
if (showMCPForm) {
|
||||
return (
|
||||
<MCPFormPanel
|
||||
onBack={handleBackFromForm}
|
||||
onSave={handleSaveMCP}
|
||||
showDeleteButton={false}
|
||||
title={localize('com_ui_add_mcp_server')}
|
||||
subtitle={localize('com_agents_mcp_info_chat')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (startupConfigLoading) {
|
||||
return <MCPPanelSkeleton />;
|
||||
}
|
||||
|
||||
if (mcpServerDefinitions.length === 0) {
|
||||
return (
|
||||
<div className="p-4 text-center text-sm text-gray-500">
|
||||
{localize('com_sidepanel_mcp_no_servers_with_vars')}
|
||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||
<div className="p-4 text-center text-sm text-gray-500">
|
||||
{localize('com_sidepanel_mcp_no_servers_with_vars')}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={handleAddMCP}
|
||||
className="w-full bg-green-500 text-white hover:bg-green-600"
|
||||
>
|
||||
{localize('com_ui_add_mcp')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -153,6 +196,12 @@ export default function MCPPanel() {
|
|||
{server.serverName}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
onClick={handleAddMCP}
|
||||
className="w-full bg-green-500 text-white hover:bg-green-600"
|
||||
>
|
||||
{localize('com_ui_add_mcp')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"com_agents_mcp_description_placeholder": "Explain what it does in a few words",
|
||||
"com_agents_mcp_icon_size": "Minimum size 128 x 128 px",
|
||||
"com_agents_mcp_info": "Add MCP servers to your agent to enable it to perform tasks and interact with external services",
|
||||
"com_agents_mcp_info_chat": "Add MCP servers to enable chat to perform tasks and interact with external services",
|
||||
"com_agents_mcp_name_placeholder": "Custom Tool",
|
||||
"com_agents_mcp_trust_subtext": "Custom connectors are not verified by LibreChat",
|
||||
"com_agents_mcps_disabled": "You need to create an agent before adding MCPs.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue