LibreChat/client/src/components/SidePanel/MCPBuilder/MCPServerCard.tsx
Danny Avila 90521bfb4e
🧹 fix: MCP Panel Regressions after UI refactor (#11312)
* fix: Revoke OAuth and Vars. Config Regressions in MCP Panel

- Introduced a new Trash2 icon button in MCPCardActions for revoking OAuth access on connected OAuth servers.
- Updated MCPServerCard to handle the revoke action, allowing users to revoke OAuth for specific servers.
- Enhanced user experience by ensuring the revoke option is available regardless of the server's connection state.

* refactor: Reorganize Revoke Button Logic in MCPCardActions and Update Toast Messages

- Moved the Revoke button for OAuth servers to a new position in MCPCardActions for improved visibility.
- Updated the success message logic in useMCPServerManager to differentiate between uninstall and variable update actions, enhancing user feedback.

* i18n: Add new translation for MCP server access revocation message

* refactor: Centralize Deselection Logic in updateUserPluginsMutation

- Updated the success handler in useUpdateUserPluginsMutation to manage deselection of MCP server values when revoking access, improving code clarity and reducing redundancy.
- Simplified message assignment logic for user feedback during plugin updates.
2026-01-12 19:01:45 -05:00

160 lines
5.6 KiB
TypeScript

import { useState, useRef } from 'react';
import { MCPIcon } from '@librechat/client';
import { PermissionBits, hasPermissions } from 'librechat-data-provider';
import type { MCPServerStatusIconProps } from '~/components/MCP/MCPServerStatusIcon';
import type { MCPServerDefinition } from '~/hooks';
import MCPServerDialog from './MCPServerDialog';
import { getStatusDotColor } from './MCPStatusBadge';
import MCPCardActions from './MCPCardActions';
import { useMCPServerManager, useLocalize } from '~/hooks';
import { cn } from '~/utils';
interface MCPServerCardProps {
server: MCPServerDefinition;
getServerStatusIconProps: (serverName: string) => MCPServerStatusIconProps;
canCreateEditMCPs: boolean;
}
/**
* Compact card component for displaying an MCP server with status and actions.
*
* Visual design:
* - Status shown via colored dot on icon (no separate badge - avoids redundancy)
* - Action buttons clearly indicate available operations
* - Consistent with MCPServerMenuItem in chat dropdown
*/
export default function MCPServerCard({
server,
getServerStatusIconProps,
canCreateEditMCPs,
}: MCPServerCardProps) {
const localize = useLocalize();
const triggerRef = useRef<HTMLDivElement>(null);
const { initializeServer, revokeOAuthForServer } = useMCPServerManager();
const [dialogOpen, setDialogOpen] = useState(false);
const statusIconProps = getServerStatusIconProps(server.serverName);
const {
serverStatus,
onConfigClick,
isInitializing,
canCancel,
onCancel,
hasCustomUserVars = false,
} = statusIconProps;
const canEditThisServer = hasPermissions(server.effectivePermissions, PermissionBits.EDIT);
const displayName = server.config?.title || server.serverName;
const description = server.config?.description;
const statusDotColor = getStatusDotColor(serverStatus, isInitializing);
const canEdit = canCreateEditMCPs && canEditThisServer;
const handleInitialize = () => {
/** If server has custom user vars and is not already connected, show config dialog first
* This ensures users can enter credentials before initialization attempts
*/
if (hasCustomUserVars && serverStatus?.connectionState !== 'connected') {
onConfigClick({ stopPropagation: () => {}, preventDefault: () => {} } as React.MouseEvent);
return;
}
initializeServer(server.serverName);
};
const handleRevoke = () => {
revokeOAuthForServer(server.serverName);
};
const handleEditClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
setDialogOpen(true);
};
// Determine status text for accessibility
const getStatusText = () => {
if (isInitializing) return localize('com_nav_mcp_status_initializing');
if (!serverStatus) return localize('com_nav_mcp_status_unknown');
const { connectionState, requiresOAuth } = serverStatus;
if (connectionState === 'connected') return localize('com_nav_mcp_status_connected');
if (connectionState === 'connecting') return localize('com_nav_mcp_status_connecting');
if (connectionState === 'error') return localize('com_nav_mcp_status_error');
if (connectionState === 'disconnected') {
return requiresOAuth
? localize('com_nav_mcp_status_needs_auth')
: localize('com_nav_mcp_status_disconnected');
}
return localize('com_nav_mcp_status_unknown');
};
return (
<>
<div
className={cn(
'group flex items-center gap-3 rounded-lg px-3 py-2.5',
'border border-border-light bg-transparent',
)}
aria-label={`${displayName} - ${getStatusText()}`}
>
{/* Server Icon with Status Dot */}
<div className="relative flex-shrink-0">
{server.config?.iconPath ? (
<img
src={server.config.iconPath}
className="size-8 rounded-lg object-cover"
alt=""
aria-hidden="true"
/>
) : (
<div className="flex size-8 items-center justify-center rounded-lg bg-surface-tertiary">
<MCPIcon className="size-5 text-text-secondary" aria-hidden="true" />
</div>
)}
{/* Status dot - color indicates connection state */}
<div
className={cn(
'absolute -bottom-0.5 -right-0.5 size-3 rounded-full',
'border-2 border-surface-primary',
statusDotColor,
(isInitializing || serverStatus?.connectionState === 'connecting') && 'animate-pulse',
)}
aria-hidden="true"
/>
</div>
{/* Server Info */}
<div className="min-w-0 flex-1">
<span className="truncate text-sm font-medium text-text-primary">{displayName}</span>
{description && <p className="truncate text-xs text-text-secondary">{description}</p>}
</div>
{/* Actions */}
<div className="flex-shrink-0">
<MCPCardActions
serverName={server.serverName}
serverStatus={serverStatus}
isInitializing={isInitializing}
canCancel={canCancel}
hasCustomUserVars={hasCustomUserVars}
canEdit={canEdit}
editButtonRef={triggerRef}
onEditClick={handleEditClick}
onConfigClick={onConfigClick}
onInitialize={handleInitialize}
onCancel={onCancel}
onRevoke={handleRevoke}
/>
</div>
</div>
{/* Edit Dialog - separate from card */}
{canEdit && (
<MCPServerDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
triggerRef={triggerRef}
server={server}
/>
)}
</>
);
}