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
|
|
|
|
🗂️ refactor: Artifacts via Model Specs & Scope Badge Persistence by Spec Context (#11796)
* 🔧 refactor: Simplify MCP selection logic in useMCPSelect hook
- Removed redundant useEffect for setting ephemeral agent when MCP values change.
- Integrated ephemeral agent update directly into the MCP value change handler, improving code clarity and reducing unnecessary re-renders.
- Updated dependencies in the effect hook to ensure proper state management.
Why Effect 2 Was Added (PR #9528)
PR #9528 was a refactor that migrated MCP state from useLocalStorage hooks to Jotai atomWithStorage. Before that PR, useLocalStorage
handled bidirectional sync between localStorage and Recoil in one abstraction. After the migration, the two useEffect hooks were
introduced to bridge Jotai ↔ Recoil:
- Effect 1 (Recoil → Jotai): When ephemeralAgent.mcp changes externally, update the Jotai atom (which drives the UI dropdown)
- Effect 2 (Jotai → Recoil): When mcpValues changes, push it back to ephemeralAgent.mcp (which is read at submission time)
Effect 2 was needed because in that PR's design, setMCPValues only wrote to Jotai — it never touched Recoil. Effect 2 was the bridge to
propagate user selections into the ephemeral agent.
Why Removing It Is Correct
All user-initiated MCP changes go through setMCPValues. The callers are in useMCPServerManager: toggleServerSelection,
batchToggleServers, OAuth success callbacks, and access revocation. Our change puts the Recoil write directly in that callback, so all
these paths are covered.
All external changes go through Recoil, handled by Effect 1 (kept). Model spec application (applyModelSpecEphemeralAgent), agent
template application after submission, and BadgeRowContext initialization all write directly to ephemeralAgentByConvoId. Effect 1
watches ephemeralAgent?.mcp and syncs those into the Jotai atom for the UI.
There is no code path where mcpValues changes without going through setMCPValues or Effect 1. The only other source is
atomWithStorage's getOnInit reading from localStorage on mount — that's just restoring persisted state and is harmless (overwritten by
Effect 1 if the ephemeral agent has values).
Additional Benefits
- Eliminates the race condition. Effect 2 fired on mount with Jotai's stale default ([]), overwriting ephemeralAgent.mcp that had been
set by a model spec. Our change prevents that because the imperative sync only fires on explicit user action.
- Eliminates infinite loop risk. The old bidirectional two-effect approach relied on isEqual/JSON.stringify checks to break cycles. The
new unidirectional-reactive (Effect 1) + imperative (setMCPValues) approach has no such risk.
- Effect 1's enhancements are preserved. The mcp_clear sentinel handling and configuredServers filtering (both added after PR #9528)
continue to work correctly.
* ✨ feat: Add artifacts support to model specifications and ephemeral agents
- Introduced `artifacts` property in the model specification and ephemeral agent types, allowing for string or boolean values.
- Updated `applyModelSpecEphemeralAgent` to handle artifacts, defaulting to 'default' if true or an empty string if not specified.
- Enhanced localStorage handling to store artifacts alongside other agent properties, improving state management for ephemeral agents.
* 🔧 refactor: Update BadgeRowContext to improve localStorage handling
- Modified the logic to only apply values from localStorage that were actually stored, preventing unnecessary overrides of the ephemeral agent.
- Simplified the setting of ephemeral agent values by directly using initialValues, enhancing code clarity and maintainability.
* 🔧 refactor: Enhance ephemeral agent handling in BadgeRowContext and model spec application
- Updated BadgeRowContext to apply localStorage values only for tools not already set in ephemeralAgent, improving state management.
- Modified useApplyModelSpecEffects to reset the ephemeral agent when no spec is provided but specs are configured, ensuring localStorage defaults are applied correctly.
- Streamlined the logic for applying model spec properties, enhancing clarity and maintainability.
* refactor: Isolate spec and non-spec tool/MCP state with environment-keyed storage
Spec tool state (badges, MCP) and non-spec user preferences previously shared
conversation-keyed localStorage, causing cross-pollination when switching between
spec and non-spec models. This introduces environment-keyed storage so each
context maintains independent persisted state.
Key changes:
- Spec active: no localStorage persistence — admin config always applied fresh
- Non-spec (with specs configured): tool/MCP state persisted to __defaults__ key
- No specs configured: zero behavior change (conversation-keyed storage)
- Per-conversation isolation preserved for existing conversations
- Dual-write on user interaction updates both conversation and environment keys
- Remove mcp_clear sentinel in favor of null ephemeral agent reset
* refactor: Enhance ephemeral agent initialization and MCP handling in BadgeRowContext and useMCPSelect
- Updated BadgeRowContext to clarify the handling of localStorage values for ephemeral agents, ensuring proper initialization based on conversation state.
- Improved useMCPSelect tests to accurately reflect behavior when setting empty MCP values, ensuring the visual selection clears as expected.
- Introduced environment-keyed storage logic to maintain independent state for spec and non-spec contexts, enhancing user experience during context switching.
* test: Add comprehensive tests for useToolToggle and applyModelSpecEphemeralAgent hooks
- Introduced unit tests for the useToolToggle hook, covering dual-write behavior in non-spec mode and per-conversation isolation.
- Added tests for applyModelSpecEphemeralAgent, ensuring correct application of model specifications and user overrides from localStorage.
- Enhanced test coverage for ephemeral agent state management during conversation transitions, validating expected behaviors for both new and existing conversations.
2026-02-14 13:18:50 -05:00
|
|
|
export function useMCPServerManager({
|
|
|
|
|
conversationId,
|
|
|
|
|
storageContextKey,
|
|
|
|
|
}: { conversationId?: string | null; storageContextKey?: string } = {}) {
|
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,
|
🗂️ refactor: Artifacts via Model Specs & Scope Badge Persistence by Spec Context (#11796)
* 🔧 refactor: Simplify MCP selection logic in useMCPSelect hook
- Removed redundant useEffect for setting ephemeral agent when MCP values change.
- Integrated ephemeral agent update directly into the MCP value change handler, improving code clarity and reducing unnecessary re-renders.
- Updated dependencies in the effect hook to ensure proper state management.
Why Effect 2 Was Added (PR #9528)
PR #9528 was a refactor that migrated MCP state from useLocalStorage hooks to Jotai atomWithStorage. Before that PR, useLocalStorage
handled bidirectional sync between localStorage and Recoil in one abstraction. After the migration, the two useEffect hooks were
introduced to bridge Jotai ↔ Recoil:
- Effect 1 (Recoil → Jotai): When ephemeralAgent.mcp changes externally, update the Jotai atom (which drives the UI dropdown)
- Effect 2 (Jotai → Recoil): When mcpValues changes, push it back to ephemeralAgent.mcp (which is read at submission time)
Effect 2 was needed because in that PR's design, setMCPValues only wrote to Jotai — it never touched Recoil. Effect 2 was the bridge to
propagate user selections into the ephemeral agent.
Why Removing It Is Correct
All user-initiated MCP changes go through setMCPValues. The callers are in useMCPServerManager: toggleServerSelection,
batchToggleServers, OAuth success callbacks, and access revocation. Our change puts the Recoil write directly in that callback, so all
these paths are covered.
All external changes go through Recoil, handled by Effect 1 (kept). Model spec application (applyModelSpecEphemeralAgent), agent
template application after submission, and BadgeRowContext initialization all write directly to ephemeralAgentByConvoId. Effect 1
watches ephemeralAgent?.mcp and syncs those into the Jotai atom for the UI.
There is no code path where mcpValues changes without going through setMCPValues or Effect 1. The only other source is
atomWithStorage's getOnInit reading from localStorage on mount — that's just restoring persisted state and is harmless (overwritten by
Effect 1 if the ephemeral agent has values).
Additional Benefits
- Eliminates the race condition. Effect 2 fired on mount with Jotai's stale default ([]), overwriting ephemeralAgent.mcp that had been
set by a model spec. Our change prevents that because the imperative sync only fires on explicit user action.
- Eliminates infinite loop risk. The old bidirectional two-effect approach relied on isEqual/JSON.stringify checks to break cycles. The
new unidirectional-reactive (Effect 1) + imperative (setMCPValues) approach has no such risk.
- Effect 1's enhancements are preserved. The mcp_clear sentinel handling and configuredServers filtering (both added after PR #9528)
continue to work correctly.
* ✨ feat: Add artifacts support to model specifications and ephemeral agents
- Introduced `artifacts` property in the model specification and ephemeral agent types, allowing for string or boolean values.
- Updated `applyModelSpecEphemeralAgent` to handle artifacts, defaulting to 'default' if true or an empty string if not specified.
- Enhanced localStorage handling to store artifacts alongside other agent properties, improving state management for ephemeral agents.
* 🔧 refactor: Update BadgeRowContext to improve localStorage handling
- Modified the logic to only apply values from localStorage that were actually stored, preventing unnecessary overrides of the ephemeral agent.
- Simplified the setting of ephemeral agent values by directly using initialValues, enhancing code clarity and maintainability.
* 🔧 refactor: Enhance ephemeral agent handling in BadgeRowContext and model spec application
- Updated BadgeRowContext to apply localStorage values only for tools not already set in ephemeralAgent, improving state management.
- Modified useApplyModelSpecEffects to reset the ephemeral agent when no spec is provided but specs are configured, ensuring localStorage defaults are applied correctly.
- Streamlined the logic for applying model spec properties, enhancing clarity and maintainability.
* refactor: Isolate spec and non-spec tool/MCP state with environment-keyed storage
Spec tool state (badges, MCP) and non-spec user preferences previously shared
conversation-keyed localStorage, causing cross-pollination when switching between
spec and non-spec models. This introduces environment-keyed storage so each
context maintains independent persisted state.
Key changes:
- Spec active: no localStorage persistence — admin config always applied fresh
- Non-spec (with specs configured): tool/MCP state persisted to __defaults__ key
- No specs configured: zero behavior change (conversation-keyed storage)
- Per-conversation isolation preserved for existing conversations
- Dual-write on user interaction updates both conversation and environment keys
- Remove mcp_clear sentinel in favor of null ephemeral agent reset
* refactor: Enhance ephemeral agent initialization and MCP handling in BadgeRowContext and useMCPSelect
- Updated BadgeRowContext to clarify the handling of localStorage values for ephemeral agents, ensuring proper initialization based on conversation state.
- Improved useMCPSelect tests to accurately reflect behavior when setting empty MCP values, ensuring the visual selection clears as expected.
- Introduced environment-keyed storage logic to maintain independent state for spec and non-spec contexts, enhancing user experience during context switching.
* test: Add comprehensive tests for useToolToggle and applyModelSpecEphemeralAgent hooks
- Introduced unit tests for the useToolToggle hook, covering dual-write behavior in non-spec mode and per-conversation isolation.
- Added tests for applyModelSpecEphemeralAgent, ensuring correct application of model specifications and user overrides from localStorage.
- Enhanced test coverage for ephemeral agent state management during conversation transitions, validating expected behaviors for both new and existing conversations.
2026-02-14 13:18:50 -05:00
|
|
|
storageContextKey,
|
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({
|
2026-01-12 19:01:45 -05:00
|
|
|
onSuccess: async (_data, variables) => {
|
|
|
|
|
const isRevoke = variables.action === 'uninstall';
|
|
|
|
|
const message = isRevoke
|
|
|
|
|
? localize('com_nav_mcp_access_revoked')
|
|
|
|
|
: localize('com_nav_mcp_vars_updated');
|
|
|
|
|
showToast({ message, status: 'success' });
|
|
|
|
|
|
|
|
|
|
/** Deselect server from mcpValues when revoking access */
|
|
|
|
|
if (isRevoke && variables.pluginKey?.startsWith(Constants.mcp_prefix)) {
|
|
|
|
|
const serverName = variables.pluginKey.replace(Constants.mcp_prefix, '');
|
|
|
|
|
const currentValues = mcpValuesRef.current ?? [];
|
|
|
|
|
const filteredValues = currentValues.filter((name) => name !== serverName);
|
|
|
|
|
setMCPValues(filteredValues);
|
|
|
|
|
}
|
2025-07-25 11:51:42 -07:00
|
|
|
|
|
|
|
|
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);
|
2026-01-12 19:01:45 -05:00
|
|
|
/** Deselection is now handled centrally in updateUserPluginsMutation.onSuccess */
|
2025-07-25 11:51:42 -07:00
|
|
|
}
|
|
|
|
|
},
|
2026-01-12 19:01:45 -05:00
|
|
|
[selectedToolForConfig, updateUserPluginsMutation],
|
2025-07-25 11:51:42 -07:00
|
|
|
);
|
|
|
|
|
|
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
|
|
|
};
|
|
|
|
|
}
|