mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-29 14:48:51 +01:00
* feat: enhance MCP server selection UI with new components and improved accessibility * fix(i18n): add missing com_ui_mcp_servers translation key The MCP server menu aria-label was referencing a non-existent translation key. Added the missing key for accessibility. * feat(MCP): enhance MCP components with improved accessibility and focus management * fix(i18n): remove outdated MCP server translation keys * fix(MCPServerList): improve color contrast by updating text color for no MCP servers message * refactor(MCP): Server status components and improve user action handling Updated MCPServerStatusIcon to use a unified icon system for better clarity Introduced new MCPCardActions component for standardized action buttons on server cards Created MCPServerCard component to encapsulate server display logic and actions Enhanced MCPServerList to render MCPServerCard components, improving code organization Added MCPStatusBadge for consistent status representation in dialogs Updated utility functions for status color and text retrieval to align with new design Improved localization keys for better clarity and consistency in user messages * style(MCP): update button and card background styles for improved UI consistency * feat(MCP): implement global server initialization state management using Jotai * refactor(MCP): modularize MCPServerDialog into structured component architecture - Split monolithic dialog into dedicated section components (Auth, BasicInfo, Connection, Transport, Trust) - Extract form logic into useMCPServerForm custom hook - Add utility modules for JSON import and URL handling - Introduce reusable SecretInput component in @librechat/client - Remove deprecated MCPAuth component * style(MCP): update button styles for improved layout and adjust empty state background color * refactor(Radio): enhance component mounting logic and background style updates * refactor(translation): remove unused keys and streamline localization strings
682 lines
23 KiB
TypeScript
682 lines
23 KiB
TypeScript
import { useCallback, useState, useMemo, useRef, useEffect } from 'react';
|
|
import { useAtom } from 'jotai';
|
|
import { useToastContext } from '@librechat/client';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { Constants, QueryKeys, MCPOptions, ResourceType } from 'librechat-data-provider';
|
|
import {
|
|
useCancelMCPOAuthMutation,
|
|
useUpdateUserPluginsMutation,
|
|
useReinitializeMCPServerMutation,
|
|
useGetAllEffectivePermissionsQuery,
|
|
} from 'librechat-data-provider/react-query';
|
|
import type { TUpdateUserPlugins, TPlugin, MCPServersResponse } from 'librechat-data-provider';
|
|
import type { ConfigFieldDetail } from '~/common';
|
|
import { useLocalize, useMCPSelect, useMCPConnectionStatus } from '~/hooks';
|
|
import { useGetStartupConfig, useMCPServersQuery } from '~/data-provider';
|
|
import { mcpServerInitStatesAtom, getServerInitState } from '~/store/mcp';
|
|
import type { MCPServerInitState } from '~/store/mcp';
|
|
|
|
export interface MCPServerDefinition {
|
|
serverName: string;
|
|
config: MCPOptions;
|
|
dbId?: string; // MongoDB ObjectId for database servers (used for permissions)
|
|
effectivePermissions: number; // Permission bits (VIEW=1, EDIT=2, DELETE=4, SHARE=8)
|
|
consumeOnly?: boolean;
|
|
}
|
|
|
|
// Poll intervals are kept local since they're timer references that can't be serialized
|
|
// The init states (isInitializing, isCancellable, etc.) are stored in the global Jotai atom
|
|
type PollIntervals = Record<string, NodeJS.Timeout | null>;
|
|
|
|
export function useMCPServerManager({ conversationId }: { conversationId?: string | null } = {}) {
|
|
const localize = useLocalize();
|
|
const queryClient = useQueryClient();
|
|
const { showToast } = useToastContext();
|
|
const { data: startupConfig } = useGetStartupConfig(); // Keep for UI config only
|
|
|
|
const { data: loadedServers, isLoading } = useMCPServersQuery();
|
|
|
|
// Fetch effective permissions for all MCP servers
|
|
const { data: permissionsMap } = useGetAllEffectivePermissionsQuery(ResourceType.MCPSERVER);
|
|
|
|
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
|
|
const [selectedToolForConfig, setSelectedToolForConfig] = useState<TPlugin | null>(null);
|
|
const previousFocusRef = useRef<HTMLElement | null>(null);
|
|
|
|
const availableMCPServers: MCPServerDefinition[] = useMemo<MCPServerDefinition[]>(() => {
|
|
const definitions: MCPServerDefinition[] = [];
|
|
if (loadedServers) {
|
|
for (const [serverName, metadata] of Object.entries(loadedServers)) {
|
|
const { dbId, consumeOnly, ...config } = metadata;
|
|
|
|
// Get effective permissions from the permissions map using _id
|
|
// Fall back to 1 (VIEW) for YAML-based servers without _id
|
|
const effectivePermissions = dbId && permissionsMap?.[dbId] ? permissionsMap[dbId] : 1;
|
|
|
|
definitions.push({
|
|
serverName,
|
|
dbId,
|
|
effectivePermissions,
|
|
consumeOnly,
|
|
config,
|
|
});
|
|
}
|
|
}
|
|
return definitions;
|
|
}, [loadedServers, permissionsMap]);
|
|
|
|
// Memoize filtered servers for useMCPSelect to prevent infinite loops
|
|
const selectableServers = useMemo(
|
|
() => availableMCPServers.filter((s) => s.config.chatMenu !== false && !s.consumeOnly),
|
|
[availableMCPServers],
|
|
);
|
|
|
|
const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({
|
|
conversationId,
|
|
servers: selectableServers,
|
|
});
|
|
const mcpValuesRef = useRef(mcpValues);
|
|
|
|
// fixes the issue where OAuth flows would deselect all the servers except the one that is being authenticated on success
|
|
useEffect(() => {
|
|
mcpValuesRef.current = mcpValues;
|
|
}, [mcpValues]);
|
|
|
|
// Check if specific permission bit is set
|
|
const checkEffectivePermission = useCallback(
|
|
(effectivePermissions: number, permissionBit: number): boolean => {
|
|
return (effectivePermissions & permissionBit) !== 0;
|
|
},
|
|
[],
|
|
);
|
|
|
|
const reinitializeMutation = useReinitializeMCPServerMutation();
|
|
const cancelOAuthMutation = useCancelMCPOAuthMutation();
|
|
|
|
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
|
|
onSuccess: async () => {
|
|
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
|
|
|
|
await Promise.all([
|
|
queryClient.invalidateQueries([QueryKeys.mcpServers]),
|
|
queryClient.invalidateQueries([QueryKeys.mcpTools]),
|
|
queryClient.invalidateQueries([QueryKeys.mcpAuthValues]),
|
|
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
|
|
]);
|
|
},
|
|
onError: (error: unknown) => {
|
|
console.error('Error updating MCP auth:', error);
|
|
showToast({
|
|
message: localize('com_nav_mcp_vars_update_error'),
|
|
status: 'error',
|
|
});
|
|
},
|
|
});
|
|
|
|
// Global atom for init states - shared across all useMCPServerManager instances
|
|
// This enables canceling OAuth from both chat dropdown and settings panel
|
|
const [serverInitStates, setServerInitStates] = useAtom(mcpServerInitStatesAtom);
|
|
|
|
// Poll intervals are kept local (not serializable)
|
|
const pollIntervalsRef = useRef<PollIntervals>({});
|
|
|
|
const { connectionStatus } = useMCPConnectionStatus({
|
|
enabled: !isLoading && availableMCPServers.length > 0,
|
|
});
|
|
|
|
const updateServerInitState = useCallback(
|
|
(serverName: string, updates: Partial<MCPServerInitState>) => {
|
|
setServerInitStates((prev) => {
|
|
const currentState = getServerInitState(prev, serverName);
|
|
return {
|
|
...prev,
|
|
[serverName]: { ...currentState, ...updates },
|
|
};
|
|
});
|
|
},
|
|
[setServerInitStates],
|
|
);
|
|
|
|
const cleanupServerState = useCallback(
|
|
(serverName: string) => {
|
|
// Clear local poll interval
|
|
const pollInterval = pollIntervalsRef.current[serverName];
|
|
if (pollInterval) {
|
|
clearTimeout(pollInterval);
|
|
pollIntervalsRef.current[serverName] = null;
|
|
}
|
|
// Reset global init state
|
|
updateServerInitState(serverName, {
|
|
isInitializing: false,
|
|
oauthUrl: null,
|
|
oauthStartTime: null,
|
|
isCancellable: false,
|
|
});
|
|
},
|
|
[updateServerInitState],
|
|
);
|
|
|
|
const startServerPolling = useCallback(
|
|
(serverName: string) => {
|
|
// Prevent duplicate polling for the same server
|
|
if (pollIntervalsRef.current[serverName]) {
|
|
console.debug(`[MCP Manager] Polling already active for ${serverName}, skipping duplicate`);
|
|
return;
|
|
}
|
|
|
|
let pollAttempts = 0;
|
|
let timeoutId: NodeJS.Timeout | null = null;
|
|
|
|
/** OAuth typically completes in 5 seconds to 3 minutes
|
|
* We enforce a strict 3-minute timeout with gradual backoff
|
|
*/
|
|
const getPollInterval = (attempt: number): number => {
|
|
if (attempt < 12) return 5000; // First minute: every 5s (12 polls)
|
|
if (attempt < 22) return 6000; // Second minute: every 6s (10 polls)
|
|
return 7500; // Final minute: every 7.5s (8 polls)
|
|
};
|
|
|
|
const maxAttempts = 30; // Exactly 3 minutes (180 seconds) total
|
|
const OAUTH_TIMEOUT_MS = 180000; // 3 minutes in milliseconds
|
|
|
|
const pollOnce = async () => {
|
|
try {
|
|
pollAttempts++;
|
|
const state = getServerInitState(serverInitStates, serverName);
|
|
|
|
/** Stop polling after 3 minutes or max attempts */
|
|
const elapsedTime = state?.oauthStartTime
|
|
? Date.now() - state.oauthStartTime
|
|
: pollAttempts * 5000; // Rough estimate if no start time
|
|
|
|
if (pollAttempts > maxAttempts || elapsedTime > OAUTH_TIMEOUT_MS) {
|
|
console.warn(
|
|
`[MCP Manager] OAuth timeout for ${serverName} after ${(elapsedTime / 1000).toFixed(0)}s (attempt ${pollAttempts})`,
|
|
);
|
|
showToast({
|
|
message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }),
|
|
status: 'error',
|
|
});
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
cleanupServerState(serverName);
|
|
return;
|
|
}
|
|
|
|
await queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]);
|
|
|
|
const freshConnectionData = queryClient.getQueryData([
|
|
QueryKeys.mcpConnectionStatus,
|
|
]) as any;
|
|
const freshConnectionStatus = freshConnectionData?.connectionStatus || {};
|
|
|
|
const serverStatus = freshConnectionStatus[serverName];
|
|
|
|
if (serverStatus?.connectionState === 'connected') {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
|
|
showToast({
|
|
message: localize('com_ui_mcp_authenticated_success', { 0: serverName }),
|
|
status: 'success',
|
|
});
|
|
|
|
const currentValues = mcpValuesRef.current ?? [];
|
|
if (!currentValues.includes(serverName)) {
|
|
setMCPValues([...currentValues, serverName]);
|
|
}
|
|
|
|
await queryClient.invalidateQueries([QueryKeys.mcpTools]);
|
|
|
|
// This delay is to ensure UI has updated with new connection status before cleanup
|
|
// Otherwise servers will show as disconnected for a second after OAuth flow completes
|
|
setTimeout(() => {
|
|
cleanupServerState(serverName);
|
|
}, 1000);
|
|
return;
|
|
}
|
|
|
|
// Check for OAuth timeout (should align with maxAttempts)
|
|
if (state?.oauthStartTime && Date.now() - state.oauthStartTime > OAUTH_TIMEOUT_MS) {
|
|
showToast({
|
|
message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }),
|
|
status: 'error',
|
|
});
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
cleanupServerState(serverName);
|
|
return;
|
|
}
|
|
|
|
if (serverStatus?.connectionState === 'error') {
|
|
showToast({
|
|
message: localize('com_ui_mcp_init_failed'),
|
|
status: 'error',
|
|
});
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
cleanupServerState(serverName);
|
|
return;
|
|
}
|
|
|
|
// Schedule next poll with smart intervals based on OAuth timing
|
|
const nextInterval = getPollInterval(pollAttempts);
|
|
|
|
// Log progress periodically
|
|
if (pollAttempts % 5 === 0 || pollAttempts <= 2) {
|
|
console.debug(
|
|
`[MCP Manager] Polling ${serverName} attempt ${pollAttempts}/${maxAttempts}, next in ${nextInterval / 1000}s`,
|
|
);
|
|
}
|
|
|
|
timeoutId = setTimeout(pollOnce, nextInterval);
|
|
pollIntervalsRef.current[serverName] = timeoutId;
|
|
} catch (error) {
|
|
console.error(`[MCP Manager] Error polling server ${serverName}:`, error);
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
cleanupServerState(serverName);
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Start the first poll
|
|
timeoutId = setTimeout(pollOnce, getPollInterval(0));
|
|
pollIntervalsRef.current[serverName] = timeoutId;
|
|
},
|
|
[queryClient, serverInitStates, showToast, localize, setMCPValues, cleanupServerState],
|
|
);
|
|
|
|
const initializeServer = useCallback(
|
|
async (serverName: string, autoOpenOAuth: boolean = true) => {
|
|
updateServerInitState(serverName, { isInitializing: true });
|
|
try {
|
|
const response = await reinitializeMutation.mutateAsync(serverName);
|
|
if (!response.success) {
|
|
showToast({
|
|
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
|
status: 'error',
|
|
});
|
|
cleanupServerState(serverName);
|
|
return response;
|
|
}
|
|
|
|
if (response.oauthRequired && response.oauthUrl) {
|
|
updateServerInitState(serverName, {
|
|
oauthUrl: response.oauthUrl,
|
|
oauthStartTime: Date.now(),
|
|
isCancellable: true,
|
|
isInitializing: true,
|
|
});
|
|
|
|
if (autoOpenOAuth) {
|
|
window.open(response.oauthUrl, '_blank', 'noopener,noreferrer');
|
|
}
|
|
|
|
startServerPolling(serverName);
|
|
} else {
|
|
await Promise.all([
|
|
queryClient.invalidateQueries([QueryKeys.mcpServers]),
|
|
queryClient.invalidateQueries([QueryKeys.mcpTools]),
|
|
queryClient.invalidateQueries([QueryKeys.mcpAuthValues]),
|
|
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
|
|
]);
|
|
|
|
showToast({
|
|
message: localize('com_ui_mcp_initialized_success', { 0: serverName }),
|
|
status: 'success',
|
|
});
|
|
|
|
const currentValues = mcpValues ?? [];
|
|
if (!currentValues.includes(serverName)) {
|
|
setMCPValues([...currentValues, serverName]);
|
|
}
|
|
|
|
cleanupServerState(serverName);
|
|
}
|
|
return response;
|
|
} catch (error) {
|
|
console.error(`[MCP Manager] Failed to initialize ${serverName}:`, error);
|
|
showToast({
|
|
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
|
status: 'error',
|
|
});
|
|
cleanupServerState(serverName);
|
|
}
|
|
},
|
|
[
|
|
updateServerInitState,
|
|
reinitializeMutation,
|
|
startServerPolling,
|
|
queryClient,
|
|
showToast,
|
|
localize,
|
|
mcpValues,
|
|
cleanupServerState,
|
|
setMCPValues,
|
|
],
|
|
);
|
|
|
|
const cancelOAuthFlow = useCallback(
|
|
(serverName: string) => {
|
|
cancelOAuthMutation.mutate(serverName, {
|
|
onSuccess: () => {
|
|
cleanupServerState(serverName);
|
|
Promise.all([
|
|
queryClient.invalidateQueries([QueryKeys.mcpServers]),
|
|
queryClient.invalidateQueries([QueryKeys.mcpTools]),
|
|
queryClient.invalidateQueries([QueryKeys.mcpAuthValues]),
|
|
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
|
|
]);
|
|
|
|
showToast({
|
|
message: localize('com_ui_mcp_oauth_cancelled', { 0: serverName }),
|
|
status: 'warning',
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
console.error(`[MCP Manager] Failed to cancel OAuth for ${serverName}:`, error);
|
|
showToast({
|
|
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
|
status: 'error',
|
|
});
|
|
},
|
|
});
|
|
},
|
|
[queryClient, cleanupServerState, showToast, localize, cancelOAuthMutation],
|
|
);
|
|
|
|
const isInitializing = useCallback(
|
|
(serverName: string) => {
|
|
return getServerInitState(serverInitStates, serverName).isInitializing;
|
|
},
|
|
[serverInitStates],
|
|
);
|
|
|
|
const isCancellable = useCallback(
|
|
(serverName: string) => {
|
|
return getServerInitState(serverInitStates, serverName).isCancellable;
|
|
},
|
|
[serverInitStates],
|
|
);
|
|
|
|
const getOAuthUrl = useCallback(
|
|
(serverName: string) => {
|
|
return getServerInitState(serverInitStates, serverName).oauthUrl;
|
|
},
|
|
[serverInitStates],
|
|
);
|
|
|
|
const placeholderText = useMemo(
|
|
() => startupConfig?.interface?.mcpServers?.placeholder || localize('com_ui_mcp_servers'),
|
|
[startupConfig?.interface?.mcpServers?.placeholder, localize],
|
|
);
|
|
|
|
const batchToggleServers = useCallback(
|
|
(serverNames: string[]) => {
|
|
const connectedServers: string[] = [];
|
|
const disconnectedServers: string[] = [];
|
|
|
|
serverNames.forEach((serverName) => {
|
|
if (isInitializing(serverName)) {
|
|
return;
|
|
}
|
|
|
|
const serverStatus = connectionStatus?.[serverName];
|
|
if (serverStatus?.connectionState === 'connected') {
|
|
connectedServers.push(serverName);
|
|
} else {
|
|
disconnectedServers.push(serverName);
|
|
}
|
|
});
|
|
|
|
setMCPValues(connectedServers);
|
|
|
|
disconnectedServers.forEach((serverName) => {
|
|
initializeServer(serverName);
|
|
});
|
|
},
|
|
[connectionStatus, setMCPValues, initializeServer, isInitializing],
|
|
);
|
|
|
|
const toggleServerSelection = useCallback(
|
|
(serverName: string) => {
|
|
if (isInitializing(serverName)) {
|
|
return;
|
|
}
|
|
|
|
const currentValues = mcpValues ?? [];
|
|
const isCurrentlySelected = currentValues.includes(serverName);
|
|
|
|
if (isCurrentlySelected) {
|
|
const filteredValues = currentValues.filter((name) => name !== serverName);
|
|
setMCPValues(filteredValues);
|
|
} else {
|
|
const serverStatus = connectionStatus?.[serverName];
|
|
if (serverStatus?.connectionState === 'connected') {
|
|
setMCPValues([...currentValues, serverName]);
|
|
} else {
|
|
initializeServer(serverName);
|
|
}
|
|
}
|
|
},
|
|
[mcpValues, setMCPValues, connectionStatus, initializeServer, isInitializing],
|
|
);
|
|
|
|
const handleConfigSave = useCallback(
|
|
(targetName: string, authData: Record<string, string>) => {
|
|
if (selectedToolForConfig && selectedToolForConfig.name === targetName) {
|
|
const payload: TUpdateUserPlugins = {
|
|
pluginKey: `${Constants.mcp_prefix}${targetName}`,
|
|
action: 'install',
|
|
auth: authData,
|
|
};
|
|
updateUserPluginsMutation.mutate(payload);
|
|
}
|
|
},
|
|
[selectedToolForConfig, updateUserPluginsMutation],
|
|
);
|
|
|
|
const handleConfigRevoke = useCallback(
|
|
(targetName: string) => {
|
|
if (selectedToolForConfig && selectedToolForConfig.name === targetName) {
|
|
const payload: TUpdateUserPlugins = {
|
|
pluginKey: `${Constants.mcp_prefix}${targetName}`,
|
|
action: 'uninstall',
|
|
auth: {},
|
|
};
|
|
updateUserPluginsMutation.mutate(payload);
|
|
|
|
const currentValues = mcpValues ?? [];
|
|
const filteredValues = currentValues.filter((name) => name !== targetName);
|
|
setMCPValues(filteredValues);
|
|
}
|
|
},
|
|
[selectedToolForConfig, updateUserPluginsMutation, mcpValues, setMCPValues],
|
|
);
|
|
|
|
/** Standalone revoke function for OAuth servers - doesn't require selectedToolForConfig */
|
|
const revokeOAuthForServer = useCallback(
|
|
(serverName: string) => {
|
|
const payload: TUpdateUserPlugins = {
|
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
|
action: 'uninstall',
|
|
auth: {},
|
|
};
|
|
updateUserPluginsMutation.mutate(payload);
|
|
},
|
|
[updateUserPluginsMutation],
|
|
);
|
|
|
|
const handleSave = useCallback(
|
|
(authData: Record<string, string>) => {
|
|
if (selectedToolForConfig) {
|
|
handleConfigSave(selectedToolForConfig.name, authData);
|
|
}
|
|
},
|
|
[selectedToolForConfig, handleConfigSave],
|
|
);
|
|
|
|
const handleRevoke = useCallback(() => {
|
|
if (selectedToolForConfig) {
|
|
handleConfigRevoke(selectedToolForConfig.name);
|
|
}
|
|
}, [selectedToolForConfig, handleConfigRevoke]);
|
|
|
|
const handleDialogOpenChange = useCallback((open: boolean) => {
|
|
setIsConfigModalOpen(open);
|
|
|
|
if (!open && previousFocusRef.current) {
|
|
setTimeout(() => {
|
|
if (previousFocusRef.current && typeof previousFocusRef.current.focus === 'function') {
|
|
previousFocusRef.current.focus();
|
|
}
|
|
previousFocusRef.current = null;
|
|
}, 0);
|
|
}
|
|
}, []);
|
|
|
|
const getServerStatusIconProps = useCallback(
|
|
(serverName: string) => {
|
|
const mcpData = queryClient.getQueryData<MCPServersResponse | undefined>([
|
|
QueryKeys.mcpTools,
|
|
]);
|
|
const serverData = mcpData?.servers?.[serverName];
|
|
const serverStatus = connectionStatus?.[serverName];
|
|
const serverConfig = loadedServers?.[serverName];
|
|
|
|
const handleConfigClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
previousFocusRef.current = document.activeElement as HTMLElement;
|
|
|
|
/** Minimal TPlugin object for the config dialog */
|
|
const configTool: TPlugin = {
|
|
name: serverName,
|
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
|
authConfig:
|
|
serverData?.authConfig ||
|
|
(serverConfig?.customUserVars
|
|
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
|
|
authField: key,
|
|
label: config.title,
|
|
description: config.description,
|
|
}))
|
|
: []),
|
|
authenticated: serverData?.authenticated ?? false,
|
|
};
|
|
setSelectedToolForConfig(configTool);
|
|
setIsConfigModalOpen(true);
|
|
};
|
|
|
|
const handleCancelClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
cancelOAuthFlow(serverName);
|
|
};
|
|
|
|
const hasCustomUserVars =
|
|
serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0;
|
|
|
|
return {
|
|
serverName,
|
|
serverStatus,
|
|
tool: serverData
|
|
? ({
|
|
name: serverName,
|
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
|
icon: serverData.icon,
|
|
authenticated: serverData.authenticated,
|
|
} as TPlugin)
|
|
: undefined,
|
|
onConfigClick: handleConfigClick,
|
|
isInitializing: isInitializing(serverName),
|
|
canCancel: isCancellable(serverName),
|
|
onCancel: handleCancelClick,
|
|
hasCustomUserVars,
|
|
};
|
|
},
|
|
[queryClient, isCancellable, isInitializing, cancelOAuthFlow, connectionStatus, loadedServers],
|
|
);
|
|
|
|
const getConfigDialogProps = useCallback(() => {
|
|
if (!selectedToolForConfig) return null;
|
|
|
|
const fieldsSchema: Record<string, ConfigFieldDetail> = {};
|
|
if (selectedToolForConfig?.authConfig) {
|
|
selectedToolForConfig.authConfig.forEach((field) => {
|
|
fieldsSchema[field.authField] = {
|
|
title: field.label || field.authField,
|
|
description: field.description,
|
|
};
|
|
});
|
|
}
|
|
|
|
const initialValues: Record<string, string> = {};
|
|
if (selectedToolForConfig?.authConfig) {
|
|
selectedToolForConfig.authConfig.forEach((field) => {
|
|
initialValues[field.authField] = '';
|
|
});
|
|
}
|
|
|
|
return {
|
|
serverName: selectedToolForConfig.name,
|
|
serverStatus: connectionStatus?.[selectedToolForConfig.name],
|
|
isOpen: isConfigModalOpen,
|
|
onOpenChange: handleDialogOpenChange,
|
|
fieldsSchema,
|
|
initialValues,
|
|
onSave: handleSave,
|
|
onRevoke: handleRevoke,
|
|
isSubmitting: updateUserPluginsMutation.isLoading,
|
|
};
|
|
}, [
|
|
selectedToolForConfig,
|
|
connectionStatus,
|
|
isConfigModalOpen,
|
|
handleDialogOpenChange,
|
|
handleSave,
|
|
handleRevoke,
|
|
updateUserPluginsMutation.isLoading,
|
|
]);
|
|
|
|
return {
|
|
availableMCPServers,
|
|
/** MCP servers filtered for chat menu selection (chatMenu !== false && !consumeOnly) */
|
|
selectableServers,
|
|
availableMCPServersMap: loadedServers,
|
|
isLoading,
|
|
connectionStatus,
|
|
initializeServer,
|
|
cancelOAuthFlow,
|
|
isInitializing,
|
|
isCancellable,
|
|
getOAuthUrl,
|
|
mcpValues,
|
|
setMCPValues,
|
|
|
|
isPinned,
|
|
setIsPinned,
|
|
placeholderText,
|
|
batchToggleServers,
|
|
toggleServerSelection,
|
|
localize,
|
|
|
|
isConfigModalOpen,
|
|
handleDialogOpenChange,
|
|
selectedToolForConfig,
|
|
setSelectedToolForConfig,
|
|
handleSave,
|
|
handleRevoke,
|
|
revokeOAuthForServer,
|
|
getServerStatusIconProps,
|
|
getConfigDialogProps,
|
|
checkEffectivePermission,
|
|
};
|
|
}
|