make MCPFormPanel agnostic to Agent / Chat context

This commit is contained in:
Dustin Healy 2025-06-26 14:58:38 -07:00
parent cf91dc3aad
commit 389ab1db77
6 changed files with 233 additions and 152 deletions

View 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}
/>
);
}

View file

@ -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} />;
}

View file

@ -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>

View file

@ -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);
}

View 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>
);

View file

@ -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.",