From 389ab1db77b63f07943167a31a1dc2a4e280110f Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:58:38 -0700 Subject: [PATCH] make MCPFormPanel agnostic to Agent / Chat context --- .../SidePanel/Agents/AgentMCPFormPanel.tsx | 112 ++++++++++++++++ .../SidePanel/Agents/AgentPanelSwitch.tsx | 4 +- .../MCPPanel.tsx => MCP/MCPFormPanel.tsx} | 120 ++++++++---------- .../SidePanel/{Agents => MCP}/MCPInput.tsx | 95 +++----------- .../src/components/SidePanel/MCP/MCPPanel.tsx | 53 +++++++- client/src/locales/en/translation.json | 1 + 6 files changed, 233 insertions(+), 152 deletions(-) create mode 100644 client/src/components/SidePanel/Agents/AgentMCPFormPanel.tsx rename client/src/components/SidePanel/{Agents/MCPPanel.tsx => MCP/MCPFormPanel.tsx} (59%) rename client/src/components/SidePanel/{Agents => MCP}/MCPInput.tsx (79%) diff --git a/client/src/components/SidePanel/Agents/AgentMCPFormPanel.tsx b/client/src/components/SidePanel/Agents/AgentMCPFormPanel.tsx new file mode 100644 index 0000000000..d5a28f3c11 --- /dev/null +++ b/client/src/components/SidePanel/Agents/AgentMCPFormPanel.tsx @@ -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 ( + + ); +} diff --git a/client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx b/client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx index 495b047b51..b4bd532f16 100644 --- a/client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanelSwitch.tsx @@ -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 ; } if (activePanel === Panel.mcp) { - return ; + return ; } return ; } diff --git a/client/src/components/SidePanel/Agents/MCPPanel.tsx b/client/src/components/SidePanel/MCP/MCPFormPanel.tsx similarity index 59% rename from client/src/components/SidePanel/Agents/MCPPanel.tsx rename to client/src/components/SidePanel/MCP/MCPFormPanel.tsx index 016e445e4f..ec2fe59f68 100644 --- a/client/src/components/SidePanel/Agents/MCPPanel.tsx +++ b/client/src/components/SidePanel/MCP/MCPFormPanel.tsx @@ -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; } -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({ - 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 (
-
- {!!mcp && ( + {!!mcp && showDeleteButton && onDelete && (
- +
diff --git a/client/src/components/SidePanel/Agents/MCPInput.tsx b/client/src/components/SidePanel/MCP/MCPInput.tsx similarity index 79% rename from client/src/components/SidePanel/Agents/MCPInput.tsx rename to client/src/components/SidePanel/MCP/MCPInput.tsx index 6c2f2ce51e..545b0f4afc 100644 --- a/client/src/components/SidePanel/Agents/MCPInput.tsx +++ b/client/src/components/SidePanel/MCP/MCPInput.tsx @@ -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>; + 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(); - const [isLoading, setIsLoading] = useState(false); const [showTools, setShowTools] = useState(false); const [selectedTools, setSelectedTools] = useState([]); @@ -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); } diff --git a/client/src/components/SidePanel/MCP/MCPPanel.tsx b/client/src/components/SidePanel/MCP/MCPPanel.tsx index aa2bf72112..5ec168af9e 100644 --- a/client/src/components/SidePanel/MCP/MCPPanel.tsx +++ b/client/src/components/SidePanel/MCP/MCPPanel.tsx @@ -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( 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 ( + + ); + } + if (startupConfigLoading) { return ; } if (mcpServerDefinitions.length === 0) { return ( -
- {localize('com_sidepanel_mcp_no_servers_with_vars')} +
+
+ {localize('com_sidepanel_mcp_no_servers_with_vars')} +
+
+ +
); } @@ -153,6 +196,12 @@ export default function MCPPanel() { {server.serverName} ))} +
); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index f1571cc657..6197fb50e5 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -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.",