1
- import { useQueryClient } from '@tanstack/react-query' ;
2
- import { Constants , QueryKeys } from 'librechat-data-provider' ;
3
- import type { TUpdateUserPlugins , TPlugin } from 'librechat-data-provider' ;
4
- import React , { memo , useCallback , useState , useMemo , useRef } from 'react' ;
5
- import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query' ;
6
- import MCPConfigDialog , { ConfigFieldDetail } from '~/components/ui/MCP/MCPConfigDialog' ;
7
- import { useMCPServerInitialization } from '~/hooks/MCP/useMCPServerInitialization' ;
1
+ import React , { memo , useCallback } from 'react' ;
2
+ import MCPConfigDialog from '~/components/ui/MCP/MCPConfigDialog' ;
8
3
import MCPServerStatusIcon from '~/components/ui/MCP/MCPServerStatusIcon' ;
9
- import { useToastContext , useBadgeRowContext } from '~/Providers' ;
10
4
import MultiSelect from '~/components/ui/MultiSelect' ;
11
5
import { MCPIcon } from '~/components/svg' ;
12
- import { useLocalize } from '~/hooks' ;
6
+ import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager ' ;
13
7
14
8
function MCPSelect ( ) {
15
- const localize = useLocalize ( ) ;
16
- const { showToast } = useToastContext ( ) ;
17
- const { mcpSelect, startupConfig } = useBadgeRowContext ( ) ;
18
- const { mcpValues, setMCPValues, mcpToolDetails, isPinned } = mcpSelect ;
19
-
20
- // Get all configured MCP servers from config that allow chat menu
21
- const configuredServers = useMemo ( ( ) => {
22
- if ( ! startupConfig ?. mcpServers ) {
23
- return [ ] ;
24
- }
25
- return Object . entries ( startupConfig . mcpServers )
26
- . filter ( ( [ , config ] ) => config . chatMenu !== false )
27
- . map ( ( [ serverName ] ) => serverName ) ;
28
- } , [ startupConfig ?. mcpServers ] ) ;
29
-
30
- const [ isConfigModalOpen , setIsConfigModalOpen ] = useState ( false ) ;
31
- const [ selectedToolForConfig , setSelectedToolForConfig ] = useState < TPlugin | null > ( null ) ;
32
- const previousFocusRef = useRef < HTMLElement | null > ( null ) ;
33
-
34
- const queryClient = useQueryClient ( ) ;
35
-
36
- const updateUserPluginsMutation = useUpdateUserPluginsMutation ( {
37
- onSuccess : async ( ) => {
38
- showToast ( { message : localize ( 'com_nav_mcp_vars_updated' ) , status : 'success' } ) ;
39
-
40
- // tools so we dont leave tools available for use in chat if we revoke and thus kill mcp server
41
- // auth values so customUserVars flags are updated in customUserVarsSection
42
- // connection status so connection indicators are updated in the dropdown
43
- await Promise . all ( [
44
- queryClient . refetchQueries ( [ QueryKeys . tools ] ) ,
45
- queryClient . refetchQueries ( [ QueryKeys . mcpAuthValues ] ) ,
46
- queryClient . refetchQueries ( [ QueryKeys . mcpConnectionStatus ] ) ,
47
- ] ) ;
48
- } ,
49
- onError : ( error : unknown ) => {
50
- console . error ( 'Error updating MCP auth:' , error ) ;
51
- showToast ( {
52
- message : localize ( 'com_nav_mcp_vars_update_error' ) ,
53
- status : 'error' ,
54
- } ) ;
55
- } ,
56
- } ) ;
57
-
58
- // Use the shared initialization hook
59
- const { initializeServer, isInitializing, connectionStatus, cancelOAuthFlow, isCancellable } =
60
- useMCPServerInitialization ( {
61
- onSuccess : ( serverName ) => {
62
- // Add to selected values after successful initialization
63
- const currentValues = mcpValues ?? [ ] ;
64
- if ( ! currentValues . includes ( serverName ) ) {
65
- setMCPValues ( [ ...currentValues , serverName ] ) ;
66
- }
67
- } ,
68
- onError : ( serverName ) => {
69
- // Find the tool/server configuration
70
- const tool = mcpToolDetails ?. find ( ( t ) => t . name === serverName ) ;
71
- const serverConfig = startupConfig ?. mcpServers ?. [ serverName ] ;
72
- const serverStatus = connectionStatus [ serverName ] ;
73
-
74
- // Check if this server would show a config button
75
- const hasAuthConfig =
76
- ( tool ?. authConfig && tool . authConfig . length > 0 ) ||
77
- ( serverConfig ?. customUserVars && Object . keys ( serverConfig . customUserVars ) . length > 0 ) ;
78
-
79
- // Only open dialog if the server would have shown a config button
80
- // (disconnected/error states always show button, connected only shows if hasAuthConfig)
81
- const wouldShowButton =
82
- ! serverStatus ||
83
- serverStatus . connectionState === 'disconnected' ||
84
- serverStatus . connectionState === 'error' ||
85
- ( serverStatus . connectionState === 'connected' && hasAuthConfig ) ;
86
-
87
- if ( ! wouldShowButton ) {
88
- return ; // Don't open dialog if no button would be shown
89
- }
90
-
91
- // Create tool object if it doesn't exist
92
- const configTool = tool || {
93
- name : serverName ,
94
- pluginKey : `${ Constants . mcp_prefix } ${ serverName } ` ,
95
- authConfig : serverConfig ?. customUserVars
96
- ? Object . entries ( serverConfig . customUserVars ) . map ( ( [ key , config ] ) => ( {
97
- authField : key ,
98
- label : config . title ,
99
- description : config . description ,
100
- } ) )
101
- : [ ] ,
102
- authenticated : false ,
103
- } ;
104
-
105
- previousFocusRef . current = document . activeElement as HTMLElement ;
106
-
107
- // Open the config dialog on error
108
- setSelectedToolForConfig ( configTool ) ;
109
- setIsConfigModalOpen ( true ) ;
110
- } ,
111
- } ) ;
9
+ const {
10
+ configuredServers,
11
+ mcpValues,
12
+ isPinned,
13
+ placeholderText,
14
+ batchToggleServers,
15
+ getServerStatusIconProps,
16
+ getConfigDialogProps,
17
+ localize,
18
+ } = useMCPServerManager ( ) ;
112
19
113
20
const renderSelectedValues = useCallback (
114
21
( values : string [ ] , placeholder ?: string ) => {
@@ -123,137 +30,9 @@ function MCPSelect() {
123
30
[ localize ] ,
124
31
) ;
125
32
126
- const handleConfigSave = useCallback (
127
- ( targetName : string , authData : Record < string , string > ) => {
128
- if ( selectedToolForConfig && selectedToolForConfig . name === targetName ) {
129
- // Use the pluginKey directly since it's already in the correct format
130
- console . log (
131
- `[MCP Select] Saving config for ${ targetName } , pluginKey: ${ `${ Constants . mcp_prefix } ${ targetName } ` } ` ,
132
- ) ;
133
- const payload : TUpdateUserPlugins = {
134
- pluginKey : `${ Constants . mcp_prefix } ${ targetName } ` ,
135
- action : 'install' ,
136
- auth : authData ,
137
- } ;
138
- updateUserPluginsMutation . mutate ( payload ) ;
139
- }
140
- } ,
141
- [ selectedToolForConfig , updateUserPluginsMutation ] ,
142
- ) ;
143
-
144
- const handleConfigRevoke = useCallback (
145
- ( targetName : string ) => {
146
- if ( selectedToolForConfig && selectedToolForConfig . name === targetName ) {
147
- // Use the pluginKey directly since it's already in the correct format
148
- const payload : TUpdateUserPlugins = {
149
- pluginKey : `${ Constants . mcp_prefix } ${ targetName } ` ,
150
- action : 'uninstall' ,
151
- auth : { } ,
152
- } ;
153
- updateUserPluginsMutation . mutate ( payload ) ;
154
-
155
- // Remove the server from selected values after revoke
156
- const currentValues = mcpValues ?? [ ] ;
157
- const filteredValues = currentValues . filter ( ( name ) => name !== targetName ) ;
158
- setMCPValues ( filteredValues ) ;
159
- }
160
- } ,
161
- [ selectedToolForConfig , updateUserPluginsMutation , mcpValues , setMCPValues ] ,
162
- ) ;
163
-
164
- const handleSave = useCallback (
165
- ( authData : Record < string , string > ) => {
166
- if ( selectedToolForConfig ) {
167
- handleConfigSave ( selectedToolForConfig . name , authData ) ;
168
- }
169
- } ,
170
- [ selectedToolForConfig , handleConfigSave ] ,
171
- ) ;
172
-
173
- const handleRevoke = useCallback ( ( ) => {
174
- if ( selectedToolForConfig ) {
175
- handleConfigRevoke ( selectedToolForConfig . name ) ;
176
- }
177
- } , [ selectedToolForConfig , handleConfigRevoke ] ) ;
178
-
179
- const handleDialogOpenChange = useCallback ( ( open : boolean ) => {
180
- setIsConfigModalOpen ( open ) ;
181
-
182
- // Restore focus when dialog closes
183
- if ( ! open && previousFocusRef . current ) {
184
- // Use setTimeout to ensure the dialog has fully closed before restoring focus
185
- setTimeout ( ( ) => {
186
- if ( previousFocusRef . current && typeof previousFocusRef . current . focus === 'function' ) {
187
- previousFocusRef . current . focus ( ) ;
188
- }
189
- previousFocusRef . current = null ;
190
- } , 0 ) ;
191
- }
192
- } , [ ] ) ;
193
-
194
- // Get connection status for all MCP servers (now from hook)
195
- // Remove the duplicate useMCPConnectionStatusQuery since it's in the hook
196
-
197
- // Modified setValue function that attempts to initialize disconnected servers
198
- const filteredSetMCPValues = useCallback (
199
- ( values : string [ ] ) => {
200
- // Separate connected and disconnected servers
201
- const connectedServers : string [ ] = [ ] ;
202
- const disconnectedServers : string [ ] = [ ] ;
203
-
204
- values . forEach ( ( serverName ) => {
205
- const serverStatus = connectionStatus [ serverName ] ;
206
- if ( serverStatus ?. connectionState === 'connected' ) {
207
- connectedServers . push ( serverName ) ;
208
- } else {
209
- disconnectedServers . push ( serverName ) ;
210
- }
211
- } ) ;
212
-
213
- // Only set connected servers as selected values
214
- setMCPValues ( connectedServers ) ;
215
-
216
- // Attempt to initialize each disconnected server (once)
217
- disconnectedServers . forEach ( ( serverName ) => {
218
- initializeServer ( serverName ) ;
219
- } ) ;
220
- } ,
221
- [ connectionStatus , setMCPValues , initializeServer ] ,
222
- ) ;
223
-
224
33
const renderItemContent = useCallback (
225
34
( serverName : string , defaultContent : React . ReactNode ) => {
226
- const tool = mcpToolDetails ?. find ( ( t ) => t . name === serverName ) ;
227
- const serverStatus = connectionStatus [ serverName ] ;
228
- const serverConfig = startupConfig ?. mcpServers ?. [ serverName ] ;
229
-
230
- const handleConfigClick = ( e : React . MouseEvent ) => {
231
- e . stopPropagation ( ) ;
232
- e . preventDefault ( ) ;
233
-
234
- previousFocusRef . current = document . activeElement as HTMLElement ;
235
-
236
- const configTool = tool || {
237
- name : serverName ,
238
- pluginKey : `${ Constants . mcp_prefix } ${ serverName } ` ,
239
- authConfig : serverConfig ?. customUserVars
240
- ? Object . entries ( serverConfig . customUserVars ) . map ( ( [ key , config ] ) => ( {
241
- authField : key ,
242
- label : config . title ,
243
- description : config . description ,
244
- } ) )
245
- : [ ] ,
246
- authenticated : false ,
247
- } ;
248
- setSelectedToolForConfig ( configTool ) ;
249
- setIsConfigModalOpen ( true ) ;
250
- } ;
251
-
252
- const handleCancelClick = ( e : React . MouseEvent ) => {
253
- e . stopPropagation ( ) ;
254
- e . preventDefault ( ) ;
255
- cancelOAuthFlow ( serverName ) ;
256
- } ;
35
+ const statusIconProps = getServerStatusIconProps ( serverName ) ;
257
36
258
37
// Common wrapper for the main content (check mark + text)
259
38
// Ensures Check & Text are adjacent and the group takes available space.
@@ -267,22 +46,7 @@ function MCPSelect() {
267
46
</ button >
268
47
) ;
269
48
270
- // Check if this server has customUserVars to configure
271
- const hasCustomUserVars =
272
- serverConfig ?. customUserVars && Object . keys ( serverConfig . customUserVars ) . length > 0 ;
273
-
274
- const statusIcon = (
275
- < MCPServerStatusIcon
276
- serverName = { serverName }
277
- serverStatus = { serverStatus }
278
- tool = { tool }
279
- onConfigClick = { handleConfigClick }
280
- isInitializing = { isInitializing ( serverName ) }
281
- canCancel = { isCancellable ( serverName ) }
282
- onCancel = { handleCancelClick }
283
- hasCustomUserVars = { hasCustomUserVars }
284
- />
285
- ) ;
49
+ const statusIcon = statusIconProps && < MCPServerStatusIcon { ...statusIconProps } /> ;
286
50
287
51
if ( statusIcon ) {
288
52
return (
@@ -295,14 +59,7 @@ function MCPSelect() {
295
59
296
60
return mainContentWrapper ;
297
61
} ,
298
- [
299
- isInitializing ,
300
- isCancellable ,
301
- mcpToolDetails ,
302
- cancelOAuthFlow ,
303
- connectionStatus ,
304
- startupConfig ?. mcpServers ,
305
- ] ,
62
+ [ getServerStatusIconProps ] ,
306
63
) ;
307
64
308
65
// Don't render if no servers are selected and not pinned
@@ -315,14 +72,14 @@ function MCPSelect() {
315
72
return null ;
316
73
}
317
74
318
- const placeholderText =
319
- startupConfig ?. interface ?. mcpServers ?. placeholder || localize ( 'com_ui_mcp_servers' ) ;
75
+ const configDialogProps = getConfigDialogProps ( ) ;
76
+
320
77
return (
321
78
< >
322
79
< MultiSelect
323
80
items = { configuredServers }
324
81
selectedValues = { mcpValues ?? [ ] }
325
- setSelectedValues = { filteredSetMCPValues }
82
+ setSelectedValues = { batchToggleServers }
326
83
defaultSelectedValues = { mcpValues ?? [ ] }
327
84
renderSelectedValues = { renderSelectedValues }
328
85
renderItemContent = { renderItemContent }
@@ -333,39 +90,7 @@ function MCPSelect() {
333
90
selectItemsClassName = "border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10"
334
91
selectClassName = "group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-all md:w-full size-9 p-2 md:p-3 bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner"
335
92
/>
336
- { selectedToolForConfig && (
337
- < MCPConfigDialog
338
- serverName = { selectedToolForConfig . name }
339
- serverStatus = { connectionStatus [ selectedToolForConfig . name ] }
340
- isOpen = { isConfigModalOpen }
341
- onOpenChange = { handleDialogOpenChange }
342
- fieldsSchema = { ( ( ) => {
343
- const schema : Record < string , ConfigFieldDetail > = { } ;
344
- if ( selectedToolForConfig ?. authConfig ) {
345
- selectedToolForConfig . authConfig . forEach ( ( field ) => {
346
- schema [ field . authField ] = {
347
- title : field . label ,
348
- description : field . description ,
349
- } ;
350
- } ) ;
351
- }
352
- return schema ;
353
- } ) ( ) }
354
- initialValues = { ( ( ) => {
355
- const initial : Record < string , string > = { } ;
356
- // Note: Actual initial values might need to be fetched if they are stored user-specifically
357
- if ( selectedToolForConfig ?. authConfig ) {
358
- selectedToolForConfig . authConfig . forEach ( ( field ) => {
359
- initial [ field . authField ] = '' ; // Or fetched value
360
- } ) ;
361
- }
362
- return initial ;
363
- } ) ( ) }
364
- onSave = { handleSave }
365
- onRevoke = { handleRevoke }
366
- isSubmitting = { updateUserPluginsMutation . isLoading }
367
- />
368
- ) }
93
+ { configDialogProps && < MCPConfigDialog { ...configDialogProps } /> }
369
94
</ >
370
95
) ;
371
96
}
0 commit comments