2025-07-28 09:25:34 -07:00
|
|
|
import { useCallback, useState, useMemo, useRef, useEffect } from 'react';
|
2025-12-28 18:20:15 +01:00
|
|
|
import { useAtom } from 'jotai';
|
📦 feat: Move Shared Components to `@librechat/client` (#8685)
* feat: init @librechat/client
* feat: Add common types and interfaces for accessibility, agents, artifacts, assistants, and tools
* feat: Add jotai as a peer dependency
* fix build client package
* feat: cleanup unused types from common/index.ts
- Remove 104 unused type exports from packages/client/src/common/index.ts
- Keep only 7 actually used exports (93% reduction)
- Add cleanup script with enhanced import pattern detection
- Support both named imports and namespace imports (* as t)
- Create automatic backups and comprehensive documentation
- Maintain type safety with build verification
- No breaking changes to existing code
Kept exports:
- TShowToast, Option, OptionWithIcon, DropdownValueSetter
- MentionOption, NotificationSeverity, MenuItemProps
Scripts: cleanup-common-types-safe.js, README-CLEANUP.md
* fix: cleanup
* fix: package; refactor: tsconfig
* feat: add back `recoil`
* fix: move dependencies to peerDependencies in client package
* feat: add @librechat/client as a dependency in package.json and package-lock.json
* feat: update client package configuration and dependencies
- Added new dependencies for Rollup plugins and updated existing ones in package.json and package-lock.json.
- Introduced a new Rollup configuration file for building the client package.
- Refactored build scripts to include a dedicated build command for the client.
- Updated TypeScript configuration for improved module resolution and type declaration output.
- Integrated a Toast component from the client package into the main App component.
* feat: enhance Rollup configuration for client package
- Updated terser plugin settings to preserve directives like 'use client'.
- Added custom warning handler to ignore "use client" directive warnings during the build process.
* chore: rename package/client build script command
* feat: update client package dependencies and Rollup configuration
- Added rollup-plugin-postcss to package.json and updated package-lock.json.
- Enhanced Rollup configuration to include postcss plugin for CSS handling.
- Updated index.ts to export all components from the components directory for better modularity.
* feat: add client package directory to update configuration
- Included the 'client' package directory in the update.js configuration to ensure it is recognized during updates.
* feat: export Toast component in client package
- Added export for the Toast component in index.ts to enhance modularity and accessibility of components.
* feat: /client transition to @librechat/client
* chore: fixed formatting issues
* fix: update peer dependencies in @librechat/client to prevent bundling them
* fix: correct useSprings implementation in SplitText component
* fix: circular dependencies in DataTable
* fix: add remaining peer dependencies and match actual versions previously used in `client/package.json`
* fix: correct frontend:ci script to include client package build
* chore: enhance unused package detection for @librechat/client and improve dependency extraction
* fix: add missing peer dependency for @radix-ui/react-collapsible
* chore: include "packages/client" in unused i18next keys detection
* test: update AgentFooter tests to use document.querySelector for spinner checks
test: mock window.matchMedia in setupTests.js for consistent test environment
* feat: add react-hook-form dependency and update FormInput component to use its types
* chore: linting
* refactor: remove unused defaultSelectedValues prop from MCPSelect and MultiSelect components
* chore: linting
* feat: update GitHub Actions workflow to publish @librechat/client
* chore: update GitHub Actions workflow to install and build data-provider and client dependencies
* chore: add missing @testing-library/react dependency to client package
* chore: update tsconfig.json to exclude additional test files
* chore: fix build issues, resolve latest LC changes
* chore: move MCP components outside of `~/components/ui`
* feat: implement dynamic theme system with environment variable support and Tailwind CSS integration
* chore: remove unnecessary logging of sttExternal and ttsExternal in Speech component
* chore: squashed cleanup commits
chore: move @tanstack/react-virtual to dependencies and remove recoil from package.json
chore: move dependencies to peerDependencies in package.json
feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration
feat: update package.json and rollup.config.js to include jotai and enhance bundling configuration
refactor: reorganize exports in index.ts for improved clarity
refactor: remove unused types and interfaces from common files
refactor: update peer dependencies and improve component typings
- Removed duplicate peer dependencies from package.json and organized them.
- Updated rollup.config.js to disable TypeScript checking during the build process.
- Modified AnimatedTabs component to use React.ReactNode for label and content types, and added TypeScript workarounds for compatibility.
- Enhanced Label and Separator components to accept an optional className prop and improved prop spreading.
- Updated Slider component to include an optional className prop and refined prop handling for better type safety.
refactor: clean up client workflow and update package dependencies
refactor: update package dependencies and improve PostCSS and Rollup configurations
chore: bump version to 0.1.2 in package.json
chore: bump client version to 0.1.2 in package-lock.json
chore: bump client version to 0.1.3 and update dependencies
chore: bump client version to 0.1.4 and update @react-spring dependencies
chore: update package version to 0.1.5 and adjust peer dependencies
- Bump version in package.json from 0.1.4 to 0.1.5.
- Update peer dependency for @tanstack/react-query to allow version 5.0.0.
- Add @tanstack/react-table and @tanstack/react-virtual as dependencies.
- Update various dependencies to their latest compatible versions.
- Simplify postcss.config.js by removing unnecessary options.
- Clean up rollup.config.js by removing ignored PostCSS warnings.
- Update CheckboxButton component to cast icon as React JSX element.
- Adjust Combobox component's class names for better styling.
- Change DropdownPopup component to use React's namespace import.
- Modify InputOTP component to use 'any' type for OTPInputContext.
- Ensure displayLabel and value in ModelParameters are converted to strings.
- Update MultiSearch component's placeholder to ensure it's a string.
- Cast selectIcon in MultiSelect as React JSX element for consistency.
- Update OGDialogTemplate to cast selectText as React JSX element.
- Initialize animationRef in PixelCard with undefined for clarity.
- Add TypeScript ignore comments in Select and SelectDropDown components for Radix UI type conflicts.
- Ensure title in SelectDropDown is a string and adjust rendering of options.
- Update useLocalize hook to cast options as any for compatibility.
refactor: code structure; chore: translations cleanup
chore: remove unused imports and clean up code in NewChat component
refactor: enhance Menu component to support custom render functions for menu items
style: update itemClassName in ToolsDropdown for improved UI consistency
fix: merge conflicts
chore: update @radix-ui/react-accordion to version 1.2.11
* refactor: remove unnecessary TypeScript type assertions in AnimatedTabs, Label, Separator, and Slider components
* feat: enhance theme system with localStorage persistence and new theme atoms
* chore: bump version of @librechat/client to 0.1.7
* chore: fix ci/cd warnings/errors related to linting and unused localization keys
* chore: update dependencies for class-variance-authority, clsx, and match-sorter
* chore: bump @librechat/client to v0.1.8
* feat: add utility colors for theme customization and remove unused tailwindConfig
* v0.1.9
---------
Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
2025-07-27 12:19:01 -04:00
|
|
|
import { useToastContext } from '@librechat/client';
|
2025-07-25 11:51:42 -07:00
|
|
|
import { useQueryClient } from '@tanstack/react-query';
|
2025-12-04 21:37:23 +01:00
|
|
|
import { Constants, QueryKeys, MCPOptions, ResourceType } from 'librechat-data-provider';
|
2025-07-28 09:25:34 -07:00
|
|
|
import {
|
2025-07-29 14:59:58 -04:00
|
|
|
useCancelMCPOAuthMutation,
|
2025-07-28 09:25:34 -07:00
|
|
|
useUpdateUserPluginsMutation,
|
|
|
|
|
useReinitializeMCPServerMutation,
|
2025-12-04 21:37:23 +01:00
|
|
|
useGetAllEffectivePermissionsQuery,
|
2025-07-28 09:25:34 -07:00
|
|
|
} from 'librechat-data-provider/react-query';
|
2025-09-21 20:19:51 -04:00
|
|
|
import type { TUpdateUserPlugins, TPlugin, MCPServersResponse } from 'librechat-data-provider';
|
2025-08-28 00:44:49 -04:00
|
|
|
import type { ConfigFieldDetail } from '~/common';
|
2025-09-21 20:19:51 -04:00
|
|
|
import { useLocalize, useMCPSelect, useMCPConnectionStatus } from '~/hooks';
|
2025-11-26 21:26:40 +01:00
|
|
|
import { useGetStartupConfig, useMCPServersQuery } from '~/data-provider';
|
2025-12-28 18:20:15 +01:00
|
|
|
import { mcpServerInitStatesAtom, getServerInitState } from '~/store/mcp';
|
|
|
|
|
import type { MCPServerInitState } from '~/store/mcp';
|
2025-11-26 21:26:40 +01:00
|
|
|
|
|
|
|
|
export interface MCPServerDefinition {
|
|
|
|
|
serverName: string;
|
|
|
|
|
config: MCPOptions;
|
2025-12-04 21:37:23 +01:00
|
|
|
dbId?: string; // MongoDB ObjectId for database servers (used for permissions)
|
2025-11-26 21:26:40 +01:00
|
|
|
effectivePermissions: number; // Permission bits (VIEW=1, EDIT=2, DELETE=4, SHARE=8)
|
2025-12-04 21:37:23 +01:00
|
|
|
consumeOnly?: boolean;
|
2025-11-26 21:26:40 +01:00
|
|
|
}
|
2025-07-25 11:51:42 -07:00
|
|
|
|
2025-12-28 18:20:15 +01:00
|
|
|
// 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>;
|
2025-07-28 09:25:34 -07:00
|
|
|
|
2025-08-29 19:57:01 -07:00
|
|
|
export function useMCPServerManager({ conversationId }: { conversationId?: string | null } = {}) {
|
2025-07-25 11:51:42 -07:00
|
|
|
const localize = useLocalize();
|
2025-08-28 00:44:49 -04:00
|
|
|
const queryClient = useQueryClient();
|
2025-07-25 11:51:42 -07:00
|
|
|
const { showToast } = useToastContext();
|
2025-11-26 21:26:40 +01:00
|
|
|
const { data: startupConfig } = useGetStartupConfig(); // Keep for UI config only
|
|
|
|
|
|
|
|
|
|
const { data: loadedServers, isLoading } = useMCPServersQuery();
|
2025-07-28 09:25:34 -07:00
|
|
|
|
2025-12-04 21:37:23 +01:00
|
|
|
// Fetch effective permissions for all MCP servers
|
|
|
|
|
const { data: permissionsMap } = useGetAllEffectivePermissionsQuery(ResourceType.MCPSERVER);
|
|
|
|
|
|
2025-07-28 09:25:34 -07:00
|
|
|
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
|
|
|
|
|
const [selectedToolForConfig, setSelectedToolForConfig] = useState<TPlugin | null>(null);
|
|
|
|
|
const previousFocusRef = useRef<HTMLElement | null>(null);
|
2025-11-26 21:26:40 +01:00
|
|
|
|
|
|
|
|
const availableMCPServers: MCPServerDefinition[] = useMemo<MCPServerDefinition[]>(() => {
|
|
|
|
|
const definitions: MCPServerDefinition[] = [];
|
|
|
|
|
if (loadedServers) {
|
|
|
|
|
for (const [serverName, metadata] of Object.entries(loadedServers)) {
|
2025-12-04 21:37:23 +01:00
|
|
|
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;
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
definitions.push({
|
|
|
|
|
serverName,
|
2025-12-04 21:37:23 +01:00
|
|
|
dbId,
|
|
|
|
|
effectivePermissions,
|
|
|
|
|
consumeOnly,
|
2025-11-26 21:26:40 +01:00
|
|
|
config,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return definitions;
|
2025-12-04 21:37:23 +01:00
|
|
|
}, [loadedServers, permissionsMap]);
|
|
|
|
|
|
|
|
|
|
// Memoize filtered servers for useMCPSelect to prevent infinite loops
|
|
|
|
|
const selectableServers = useMemo(
|
|
|
|
|
() => availableMCPServers.filter((s) => s.config.chatMenu !== false && !s.consumeOnly),
|
|
|
|
|
[availableMCPServers],
|
|
|
|
|
);
|
2025-11-26 21:26:40 +01:00
|
|
|
|
|
|
|
|
const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({
|
|
|
|
|
conversationId,
|
2025-12-04 21:37:23 +01:00
|
|
|
servers: selectableServers,
|
2025-11-26 21:26:40 +01:00
|
|
|
});
|
2025-07-28 09:25:34 -07:00
|
|
|
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]);
|
2025-07-25 11:51:42 -07:00
|
|
|
|
2025-12-04 21:37:23 +01:00
|
|
|
// Check if specific permission bit is set
|
|
|
|
|
const checkEffectivePermission = useCallback(
|
|
|
|
|
(effectivePermissions: number, permissionBit: number): boolean => {
|
|
|
|
|
return (effectivePermissions & permissionBit) !== 0;
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-28 09:25:34 -07:00
|
|
|
const reinitializeMutation = useReinitializeMCPServerMutation();
|
|
|
|
|
const cancelOAuthMutation = useCancelMCPOAuthMutation();
|
2025-07-25 11:51:42 -07:00
|
|
|
|
|
|
|
|
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
|
|
|
|
|
onSuccess: async () => {
|
|
|
|
|
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
2025-11-26 21:26:40 +01:00
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpServers]),
|
2025-09-22 08:53:19 -04:00
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpTools]),
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpAuthValues]),
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
|
2025-07-25 11:51:42 -07:00
|
|
|
]);
|
|
|
|
|
},
|
|
|
|
|
onError: (error: unknown) => {
|
|
|
|
|
console.error('Error updating MCP auth:', error);
|
|
|
|
|
showToast({
|
|
|
|
|
message: localize('com_nav_mcp_vars_update_error'),
|
|
|
|
|
status: 'error',
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-28 18:20:15 +01:00
|
|
|
// 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>({});
|
2025-07-28 09:25:34 -07:00
|
|
|
|
2025-08-29 19:57:01 -07:00
|
|
|
const { connectionStatus } = useMCPConnectionStatus({
|
2025-12-04 21:37:23 +01:00
|
|
|
enabled: !isLoading && availableMCPServers.length > 0,
|
2025-08-06 23:31:05 -07:00
|
|
|
});
|
2025-07-28 09:25:34 -07:00
|
|
|
|
2025-12-28 18:20:15 +01:00
|
|
|
const updateServerInitState = useCallback(
|
|
|
|
|
(serverName: string, updates: Partial<MCPServerInitState>) => {
|
|
|
|
|
setServerInitStates((prev) => {
|
|
|
|
|
const currentState = getServerInitState(prev, serverName);
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
[serverName]: { ...currentState, ...updates },
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[setServerInitStates],
|
|
|
|
|
);
|
2025-07-28 09:25:34 -07:00
|
|
|
|
|
|
|
|
const cleanupServerState = useCallback(
|
|
|
|
|
(serverName: string) => {
|
2025-12-28 18:20:15 +01:00
|
|
|
// Clear local poll interval
|
|
|
|
|
const pollInterval = pollIntervalsRef.current[serverName];
|
|
|
|
|
if (pollInterval) {
|
|
|
|
|
clearTimeout(pollInterval);
|
|
|
|
|
pollIntervalsRef.current[serverName] = null;
|
2025-07-28 09:25:34 -07:00
|
|
|
}
|
2025-12-28 18:20:15 +01:00
|
|
|
// Reset global init state
|
|
|
|
|
updateServerInitState(serverName, {
|
2025-07-28 09:25:34 -07:00
|
|
|
isInitializing: false,
|
|
|
|
|
oauthUrl: null,
|
|
|
|
|
oauthStartTime: null,
|
|
|
|
|
isCancellable: false,
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-12-28 18:20:15 +01:00
|
|
|
[updateServerInitState],
|
2025-07-28 09:25:34 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const startServerPolling = useCallback(
|
|
|
|
|
(serverName: string) => {
|
2025-09-21 22:58:19 -04:00
|
|
|
// Prevent duplicate polling for the same server
|
2025-12-28 18:20:15 +01:00
|
|
|
if (pollIntervalsRef.current[serverName]) {
|
2025-09-21 22:58:19 -04:00
|
|
|
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 () => {
|
2025-07-28 09:25:34 -07:00
|
|
|
try {
|
2025-09-21 22:58:19 -04:00
|
|
|
pollAttempts++;
|
2025-12-28 18:20:15 +01:00
|
|
|
const state = getServerInitState(serverInitStates, serverName);
|
2025-09-21 22:58:19 -04:00
|
|
|
|
|
|
|
|
/** 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 09:25:34 -07:00
|
|
|
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') {
|
2025-09-21 22:58:19 -04:00
|
|
|
if (timeoutId) {
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
}
|
2025-07-28 09:25:34 -07:00
|
|
|
|
|
|
|
|
showToast({
|
|
|
|
|
message: localize('com_ui_mcp_authenticated_success', { 0: serverName }),
|
|
|
|
|
status: 'success',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const currentValues = mcpValuesRef.current ?? [];
|
|
|
|
|
if (!currentValues.includes(serverName)) {
|
|
|
|
|
setMCPValues([...currentValues, serverName]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 07:56:40 -04:00
|
|
|
await queryClient.invalidateQueries([QueryKeys.mcpTools]);
|
2025-08-06 23:31:05 -07:00
|
|
|
|
2025-07-28 09:25:34 -07:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 22:58:19 -04:00
|
|
|
// Check for OAuth timeout (should align with maxAttempts)
|
|
|
|
|
if (state?.oauthStartTime && Date.now() - state.oauthStartTime > OAUTH_TIMEOUT_MS) {
|
2025-07-28 09:25:34 -07:00
|
|
|
showToast({
|
|
|
|
|
message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }),
|
|
|
|
|
status: 'error',
|
|
|
|
|
});
|
2025-09-21 22:58:19 -04:00
|
|
|
if (timeoutId) {
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
}
|
2025-07-28 09:25:34 -07:00
|
|
|
cleanupServerState(serverName);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (serverStatus?.connectionState === 'error') {
|
|
|
|
|
showToast({
|
|
|
|
|
message: localize('com_ui_mcp_init_failed'),
|
|
|
|
|
status: 'error',
|
|
|
|
|
});
|
2025-09-21 22:58:19 -04:00
|
|
|
if (timeoutId) {
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
}
|
2025-07-28 09:25:34 -07:00
|
|
|
cleanupServerState(serverName);
|
2025-07-29 11:54:07 -07:00
|
|
|
return;
|
2025-07-28 09:25:34 -07:00
|
|
|
}
|
2025-09-21 22:58:19 -04:00
|
|
|
|
|
|
|
|
// 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);
|
2025-12-28 18:20:15 +01:00
|
|
|
pollIntervalsRef.current[serverName] = timeoutId;
|
2025-07-28 09:25:34 -07:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`[MCP Manager] Error polling server ${serverName}:`, error);
|
2025-09-21 22:58:19 -04:00
|
|
|
if (timeoutId) {
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
}
|
2025-07-29 11:54:07 -07:00
|
|
|
cleanupServerState(serverName);
|
|
|
|
|
return;
|
2025-07-25 11:51:42 -07:00
|
|
|
}
|
2025-09-21 22:58:19 -04:00
|
|
|
};
|
2025-07-28 09:25:34 -07:00
|
|
|
|
2025-09-21 22:58:19 -04:00
|
|
|
// Start the first poll
|
|
|
|
|
timeoutId = setTimeout(pollOnce, getPollInterval(0));
|
2025-12-28 18:20:15 +01:00
|
|
|
pollIntervalsRef.current[serverName] = timeoutId;
|
2025-07-28 09:25:34 -07:00
|
|
|
},
|
2025-12-28 18:20:15 +01:00
|
|
|
[queryClient, serverInitStates, showToast, localize, setMCPValues, cleanupServerState],
|
2025-07-28 09:25:34 -07:00
|
|
|
);
|
2025-07-25 11:51:42 -07:00
|
|
|
|
2025-07-28 09:25:34 -07:00
|
|
|
const initializeServer = useCallback(
|
2025-07-29 11:54:07 -07:00
|
|
|
async (serverName: string, autoOpenOAuth: boolean = true) => {
|
2025-12-28 18:20:15 +01:00
|
|
|
updateServerInitState(serverName, { isInitializing: true });
|
2025-07-28 09:25:34 -07:00
|
|
|
try {
|
|
|
|
|
const response = await reinitializeMutation.mutateAsync(serverName);
|
2025-08-29 19:57:01 -07:00
|
|
|
if (!response.success) {
|
|
|
|
|
showToast({
|
|
|
|
|
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
|
|
|
|
status: 'error',
|
|
|
|
|
});
|
|
|
|
|
cleanupServerState(serverName);
|
|
|
|
|
return response;
|
|
|
|
|
}
|
2025-07-25 11:51:42 -07:00
|
|
|
|
2025-08-29 19:57:01 -07:00
|
|
|
if (response.oauthRequired && response.oauthUrl) {
|
2025-12-28 18:20:15 +01:00
|
|
|
updateServerInitState(serverName, {
|
2025-08-29 19:57:01 -07:00
|
|
|
oauthUrl: response.oauthUrl,
|
|
|
|
|
oauthStartTime: Date.now(),
|
|
|
|
|
isCancellable: true,
|
|
|
|
|
isInitializing: true,
|
|
|
|
|
});
|
2025-07-28 09:25:34 -07:00
|
|
|
|
2025-08-29 19:57:01 -07:00
|
|
|
if (autoOpenOAuth) {
|
|
|
|
|
window.open(response.oauthUrl, '_blank', 'noopener,noreferrer');
|
2025-07-28 09:25:34 -07:00
|
|
|
}
|
2025-08-29 19:57:01 -07:00
|
|
|
|
|
|
|
|
startServerPolling(serverName);
|
2025-07-29 06:08:46 -07:00
|
|
|
} else {
|
2025-11-26 21:26:40 +01:00
|
|
|
await Promise.all([
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpServers]),
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpTools]),
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpAuthValues]),
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
|
|
|
|
|
]);
|
2025-08-29 19:57:01 -07:00
|
|
|
|
2025-07-29 06:08:46 -07:00
|
|
|
showToast({
|
2025-08-29 19:57:01 -07:00
|
|
|
message: localize('com_ui_mcp_initialized_success', { 0: serverName }),
|
|
|
|
|
status: 'success',
|
2025-07-29 06:08:46 -07:00
|
|
|
});
|
2025-08-29 19:57:01 -07:00
|
|
|
|
|
|
|
|
const currentValues = mcpValues ?? [];
|
|
|
|
|
if (!currentValues.includes(serverName)) {
|
|
|
|
|
setMCPValues([...currentValues, serverName]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 06:08:46 -07:00
|
|
|
cleanupServerState(serverName);
|
2025-07-25 11:51:42 -07:00
|
|
|
}
|
2025-08-29 19:57:01 -07:00
|
|
|
return response;
|
2025-07-28 09:25:34 -07:00
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[
|
2025-12-28 18:20:15 +01:00
|
|
|
updateServerInitState,
|
2025-07-28 09:25:34 -07:00
|
|
|
reinitializeMutation,
|
|
|
|
|
startServerPolling,
|
|
|
|
|
queryClient,
|
|
|
|
|
showToast,
|
|
|
|
|
localize,
|
|
|
|
|
mcpValues,
|
|
|
|
|
cleanupServerState,
|
|
|
|
|
setMCPValues,
|
|
|
|
|
],
|
|
|
|
|
);
|
2025-07-25 11:51:42 -07:00
|
|
|
|
2025-07-28 09:25:34 -07:00
|
|
|
const cancelOAuthFlow = useCallback(
|
|
|
|
|
(serverName: string) => {
|
2025-07-29 11:54:07 -07:00
|
|
|
cancelOAuthMutation.mutate(serverName, {
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
cleanupServerState(serverName);
|
2025-12-04 21:37:23 +01:00
|
|
|
Promise.all([
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpServers]),
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpTools]),
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpAuthValues]),
|
|
|
|
|
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
|
|
|
|
|
]);
|
2025-07-25 11:51:42 -07:00
|
|
|
|
2025-07-29 11:54:07 -07:00
|
|
|
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',
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-07-28 09:25:34 -07:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[queryClient, cleanupServerState, showToast, localize, cancelOAuthMutation],
|
|
|
|
|
);
|
2025-07-25 11:51:42 -07:00
|
|
|
|
2025-07-28 09:25:34 -07:00
|
|
|
const isInitializing = useCallback(
|
|
|
|
|
(serverName: string) => {
|
2025-12-28 18:20:15 +01:00
|
|
|
return getServerInitState(serverInitStates, serverName).isInitializing;
|
2025-07-28 09:25:34 -07:00
|
|
|
},
|
2025-12-28 18:20:15 +01:00
|
|
|
[serverInitStates],
|
2025-07-28 09:25:34 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const isCancellable = useCallback(
|
|
|
|
|
(serverName: string) => {
|
2025-12-28 18:20:15 +01:00
|
|
|
return getServerInitState(serverInitStates, serverName).isCancellable;
|
2025-07-28 09:25:34 -07:00
|
|
|
},
|
2025-12-28 18:20:15 +01:00
|
|
|
[serverInitStates],
|
2025-07-28 09:25:34 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const getOAuthUrl = useCallback(
|
|
|
|
|
(serverName: string) => {
|
2025-12-28 18:20:15 +01:00
|
|
|
return getServerInitState(serverInitStates, serverName).oauthUrl;
|
2025-07-28 09:25:34 -07:00
|
|
|
},
|
2025-12-28 18:20:15 +01:00
|
|
|
[serverInitStates],
|
2025-07-28 09:25:34 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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) => {
|
2025-07-29 11:54:07 -07:00
|
|
|
if (isInitializing(serverName)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-29 19:57:01 -07:00
|
|
|
const serverStatus = connectionStatus?.[serverName];
|
2025-07-28 09:25:34 -07:00
|
|
|
if (serverStatus?.connectionState === 'connected') {
|
|
|
|
|
connectedServers.push(serverName);
|
|
|
|
|
} else {
|
|
|
|
|
disconnectedServers.push(serverName);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setMCPValues(connectedServers);
|
|
|
|
|
|
|
|
|
|
disconnectedServers.forEach((serverName) => {
|
|
|
|
|
initializeServer(serverName);
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-07-29 11:54:07 -07:00
|
|
|
[connectionStatus, setMCPValues, initializeServer, isInitializing],
|
2025-07-28 09:25:34 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const toggleServerSelection = useCallback(
|
|
|
|
|
(serverName: string) => {
|
2025-07-29 11:54:07 -07:00
|
|
|
if (isInitializing(serverName)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 09:25:34 -07:00
|
|
|
const currentValues = mcpValues ?? [];
|
|
|
|
|
const isCurrentlySelected = currentValues.includes(serverName);
|
|
|
|
|
|
|
|
|
|
if (isCurrentlySelected) {
|
|
|
|
|
const filteredValues = currentValues.filter((name) => name !== serverName);
|
|
|
|
|
setMCPValues(filteredValues);
|
|
|
|
|
} else {
|
2025-08-29 19:57:01 -07:00
|
|
|
const serverStatus = connectionStatus?.[serverName];
|
2025-07-28 09:25:34 -07:00
|
|
|
if (serverStatus?.connectionState === 'connected') {
|
|
|
|
|
setMCPValues([...currentValues, serverName]);
|
|
|
|
|
} else {
|
|
|
|
|
initializeServer(serverName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-07-29 11:54:07 -07:00
|
|
|
[mcpValues, setMCPValues, connectionStatus, initializeServer, isInitializing],
|
2025-07-28 09:25:34 -07:00
|
|
|
);
|
2025-07-25 11:51:42 -07:00
|
|
|
|
|
|
|
|
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],
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-04 19:52:32 -05:00
|
|
|
/** 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],
|
|
|
|
|
);
|
|
|
|
|
|
2025-07-25 11:51:42 -07:00
|
|
|
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) => {
|
2025-09-21 20:19:51 -04:00
|
|
|
const mcpData = queryClient.getQueryData<MCPServersResponse | undefined>([
|
|
|
|
|
QueryKeys.mcpTools,
|
|
|
|
|
]);
|
|
|
|
|
const serverData = mcpData?.servers?.[serverName];
|
2025-08-29 19:57:01 -07:00
|
|
|
const serverStatus = connectionStatus?.[serverName];
|
2025-11-26 21:26:40 +01:00
|
|
|
const serverConfig = loadedServers?.[serverName];
|
2025-07-25 11:51:42 -07:00
|
|
|
|
|
|
|
|
const handleConfigClick = (e: React.MouseEvent) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
previousFocusRef.current = document.activeElement as HTMLElement;
|
|
|
|
|
|
2025-09-21 20:19:51 -04:00
|
|
|
/** Minimal TPlugin object for the config dialog */
|
|
|
|
|
const configTool: TPlugin = {
|
2025-07-25 11:51:42 -07:00
|
|
|
name: serverName,
|
|
|
|
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
2025-09-21 20:19:51 -04:00
|
|
|
authConfig:
|
|
|
|
|
serverData?.authConfig ||
|
|
|
|
|
(serverConfig?.customUserVars
|
|
|
|
|
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
|
|
|
|
|
authField: key,
|
|
|
|
|
label: config.title,
|
|
|
|
|
description: config.description,
|
|
|
|
|
}))
|
|
|
|
|
: []),
|
|
|
|
|
authenticated: serverData?.authenticated ?? false,
|
2025-07-25 11:51:42 -07:00
|
|
|
};
|
|
|
|
|
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,
|
2025-09-21 20:19:51 -04:00
|
|
|
tool: serverData
|
|
|
|
|
? ({
|
|
|
|
|
name: serverName,
|
|
|
|
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
|
|
|
|
icon: serverData.icon,
|
|
|
|
|
authenticated: serverData.authenticated,
|
|
|
|
|
} as TPlugin)
|
|
|
|
|
: undefined,
|
2025-07-25 11:51:42 -07:00
|
|
|
onConfigClick: handleConfigClick,
|
|
|
|
|
isInitializing: isInitializing(serverName),
|
|
|
|
|
canCancel: isCancellable(serverName),
|
|
|
|
|
onCancel: handleCancelClick,
|
|
|
|
|
hasCustomUserVars,
|
|
|
|
|
};
|
|
|
|
|
},
|
2025-11-26 21:26:40 +01:00
|
|
|
[queryClient, isCancellable, isInitializing, cancelOAuthFlow, connectionStatus, loadedServers],
|
2025-07-25 11:51:42 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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,
|
2025-08-29 19:57:01 -07:00
|
|
|
serverStatus: connectionStatus?.[selectedToolForConfig.name],
|
2025-07-25 11:51:42 -07:00
|
|
|
isOpen: isConfigModalOpen,
|
|
|
|
|
onOpenChange: handleDialogOpenChange,
|
|
|
|
|
fieldsSchema,
|
|
|
|
|
initialValues,
|
|
|
|
|
onSave: handleSave,
|
|
|
|
|
onRevoke: handleRevoke,
|
|
|
|
|
isSubmitting: updateUserPluginsMutation.isLoading,
|
|
|
|
|
};
|
|
|
|
|
}, [
|
|
|
|
|
selectedToolForConfig,
|
|
|
|
|
connectionStatus,
|
|
|
|
|
isConfigModalOpen,
|
|
|
|
|
handleDialogOpenChange,
|
|
|
|
|
handleSave,
|
|
|
|
|
handleRevoke,
|
|
|
|
|
updateUserPluginsMutation.isLoading,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return {
|
2025-11-26 21:26:40 +01:00
|
|
|
availableMCPServers,
|
2025-12-04 21:37:23 +01:00
|
|
|
/** MCP servers filtered for chat menu selection (chatMenu !== false && !consumeOnly) */
|
|
|
|
|
selectableServers,
|
2025-11-26 21:26:40 +01:00
|
|
|
availableMCPServersMap: loadedServers,
|
|
|
|
|
isLoading,
|
|
|
|
|
connectionStatus,
|
2025-07-28 09:25:34 -07:00
|
|
|
initializeServer,
|
|
|
|
|
cancelOAuthFlow,
|
|
|
|
|
isInitializing,
|
|
|
|
|
isCancellable,
|
|
|
|
|
getOAuthUrl,
|
2025-07-25 11:51:42 -07:00
|
|
|
mcpValues,
|
2025-07-28 09:25:34 -07:00
|
|
|
setMCPValues,
|
|
|
|
|
|
2025-07-25 11:51:42 -07:00
|
|
|
isPinned,
|
|
|
|
|
setIsPinned,
|
|
|
|
|
placeholderText,
|
|
|
|
|
batchToggleServers,
|
2025-07-28 09:25:34 -07:00
|
|
|
toggleServerSelection,
|
|
|
|
|
localize,
|
2025-07-25 11:51:42 -07:00
|
|
|
|
|
|
|
|
isConfigModalOpen,
|
2025-07-28 09:25:34 -07:00
|
|
|
handleDialogOpenChange,
|
|
|
|
|
selectedToolForConfig,
|
|
|
|
|
setSelectedToolForConfig,
|
|
|
|
|
handleSave,
|
|
|
|
|
handleRevoke,
|
2025-12-04 19:52:32 -05:00
|
|
|
revokeOAuthForServer,
|
2025-07-28 09:25:34 -07:00
|
|
|
getServerStatusIconProps,
|
2025-07-25 11:51:42 -07:00
|
|
|
getConfigDialogProps,
|
2025-12-04 21:37:23 +01:00
|
|
|
checkEffectivePermission,
|
2025-07-25 11:51:42 -07:00
|
|
|
};
|
|
|
|
|
}
|