mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-26 04:06:12 +01:00
* refactor: MCP UI Separation for Agents (Dustin WIP)
feat: separate MCPs into their own lists away from tools + actions and add the status indicator functionality from chat to their dropdown ui
fix: spotify mcp was not persisting on agent creation
feat: show disconnected saved servers and their tools in agent mcp list in created agents
fix: select-all regression fixed (caused by deleting tools we were drawing from for rendering list)
fix: dont show all mcps, only those installed in agent in list
feat: separate ToolSelectDialog for MCPServerTools
fix: uninitialized mcp servers not showing as added in toolselectdialog
refactor: reduce looping in AgentPanelContext for categorizing groups and mcps
refactor: split ToolSelectDialog and MCPToolSelectDialog functionality (still needs customization for custom user vars)
chore: address ESLint comments
chore: address ESLint comments
feat: one-click initialization on MCP servers in agent builder
fix: stop propagation triggering reinit on caret click
refactor: split uninitialized MCPs component from initialized MCPs
feat: new mcp tool select dialog ui with custom user vars
feat: show initialization state for CUV configurable MCPs too
chore: remove unused localization string
fix: deselecting all tools caused a re-render
fix: remove subtools so removal from MCPToolSelectDialog works more consistently
feat: added servers have all tools enabled by default
feat: mcp server list now alphabetical to prevent annoying ui behavior of servers jumping around depending on tool selection
fix: filter out placeholder group mcp tools from any actual tool calls / definitions
feat: indicator now takes you to config dialog for uninitialized servers
feat: show previously configured mcp servers that are now missing from the yaml
feat: select all enabled by default on first add to mcp server list
chore: address ESLint comments
* refactor: MCP UI Separation for Agents (Danny WIP)
chore: remove use of `{serverName}_mcp_{serverName}`
chore: import order
WIP: separate component concerns
refactor: streamline agent mcp tools
refactor: unify MCP server handling and improve tool visibility logic, remove unnecessary normalization or sorting, remove nesting button, make variable names clear
refactor: rename mcpServerIds to mcpServerNames for clarity and consistency across components
refactor: remove groupedMCPTools and toolToServerMap, streamline MCP server handling in context and components to effectively utilize mcpServersMap
refactor: optimize tool selection logic by replacing array includes with Set for improved performance
chore: add error logging for failed auth URL parsing in ToolCall component
refactor: enhance MCP tool handling by improving server name management and updating UI elements for better clarity
* refactor: decouple connection status from useMCPServerManager with useMCPConnectionStatus
* fix: improve MCP tool validation logic to handle unconfigured servers
* chore: enhance log message clarity for MCP server disconnection in updateUserPluginsController
* refactor: simplify connection status extraction in useMCPConnectionStatus hook
* refactor: improve initializing UX
* chore: replace string literal with ResourceType constant in useResourcePermissions
* refactor: cleanup code, remove redundancies, rename variables for clarity
* chore: add back filtering and sorting for mcp tools dialog
* refactor: initializeServer to return response and early return
* refactor: enhance server initialization logic and improve UI for OAuth interaction
* chore: clarify warning message for unconfigured MCP server in handleTools
* refactor: prevent CustomUserVarsSection from submitting tools dialog form
* fix: nested button of button issue in UninitializedMCPTool
* feat: add functionality to revoke custom user variables in MCPToolSelectDialog
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
import DOMPurify from 'dompurify';
|
|
import * as Ariakit from '@ariakit/react';
|
|
import { forwardRef, useId, useMemo } from 'react';
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
import { cn } from '~/utils';
|
|
import './Tooltip.css';
|
|
|
|
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
|
role?: string;
|
|
className?: string;
|
|
description: string;
|
|
enableHTML?: boolean;
|
|
side?: 'top' | 'bottom' | 'left' | 'right';
|
|
}
|
|
|
|
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
|
|
{ description, side = 'top', className, role, enableHTML = false, ...props },
|
|
ref,
|
|
) {
|
|
const tooltip = Ariakit.useTooltipStore({ placement: side });
|
|
const mounted = Ariakit.useStoreState(tooltip, (state) => state.mounted);
|
|
const placement = Ariakit.useStoreState(tooltip, (state) => state.placement);
|
|
|
|
const id = useId();
|
|
const sanitizer = useMemo(() => {
|
|
const instance = DOMPurify();
|
|
instance.addHook('afterSanitizeAttributes', (node) => {
|
|
if (node.tagName && node.tagName === 'A') {
|
|
node.setAttribute('target', '_blank');
|
|
node.setAttribute('rel', 'noopener noreferrer');
|
|
}
|
|
});
|
|
return instance;
|
|
}, []);
|
|
|
|
const sanitizedHTML = useMemo(() => {
|
|
if (!enableHTML) {
|
|
return '';
|
|
}
|
|
try {
|
|
return sanitizer.sanitize(description, {
|
|
ALLOWED_TAGS: ['a', 'strong', 'b', 'em', 'i', 'br', 'code'],
|
|
ALLOWED_ATTR: ['href', 'class', 'target', 'rel'],
|
|
ALLOW_DATA_ATTR: false,
|
|
ALLOW_ARIA_ATTR: false,
|
|
});
|
|
} catch (error) {
|
|
console.error('Sanitization failed', error);
|
|
return description;
|
|
}
|
|
}, [enableHTML, description, sanitizer]);
|
|
|
|
const { x, y } = useMemo(() => {
|
|
const dir = placement.split('-')[0];
|
|
switch (dir) {
|
|
case 'top':
|
|
return { x: 0, y: -8 };
|
|
case 'bottom':
|
|
return { x: 0, y: 8 };
|
|
case 'left':
|
|
return { x: -8, y: 0 };
|
|
case 'right':
|
|
return { x: 8, y: 0 };
|
|
default:
|
|
return { x: 0, y: 0 };
|
|
}
|
|
}, [placement]);
|
|
|
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
if (role === 'button' && event.key === 'Enter') {
|
|
event.preventDefault();
|
|
(event.target as HTMLDivElement).click();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Ariakit.TooltipProvider store={tooltip} hideTimeout={0}>
|
|
<Ariakit.TooltipAnchor
|
|
{...props}
|
|
ref={ref}
|
|
role={role}
|
|
aria-describedby={id}
|
|
onKeyDown={handleKeyDown}
|
|
className={cn('cursor-pointer', className)}
|
|
/>
|
|
<AnimatePresence>
|
|
{mounted === true && (
|
|
<Ariakit.Tooltip
|
|
gutter={4}
|
|
alwaysVisible
|
|
className="tooltip"
|
|
id={id}
|
|
render={
|
|
<motion.div
|
|
initial={{ opacity: 0, x, y }}
|
|
animate={{ opacity: 1, x: 0, y: 0 }}
|
|
exit={{ opacity: 0, x, y }}
|
|
/>
|
|
}
|
|
>
|
|
<Ariakit.TooltipArrow />
|
|
{enableHTML ? (
|
|
<div
|
|
dangerouslySetInnerHTML={{
|
|
__html: sanitizedHTML,
|
|
}}
|
|
/>
|
|
) : (
|
|
description
|
|
)}
|
|
</Ariakit.Tooltip>
|
|
)}
|
|
</AnimatePresence>
|
|
</Ariakit.TooltipProvider>
|
|
);
|
|
});
|