@@ -16,10 +16,13 @@ import { connectMcpToAsk, getMcpServersWithStatus } from "@/app/api/(client)/cli
1616import { useToast } from "@/components/hooks/use-toast" ;
1717import { McpFavicon } from "@/ee/features/chat/mcp/components/mcpFavicon" ;
1818import { mcpQueryKeys } from "@/ee/features/chat/mcp/queryKeys" ;
19+ import { ErrorCode } from "@/lib/errorCodes" ;
20+ import type { ServiceError } from "@/lib/serviceError" ;
1921import { isServiceError } from "@/lib/utils" ;
2022import { useQuery , useQueryClient } from "@tanstack/react-query" ;
21- import { AlertTriangleIcon , CableIcon , Loader2Icon , PlusCircleIcon , PlusIcon , RefreshCwIcon , SettingsIcon } from "lucide-react" ;
22- import { useRouter } from "next/navigation" ;
23+ import { AlertTriangleIcon , CableIcon , Loader2Icon , LogInIcon , PlusCircleIcon , PlusIcon , RefreshCwIcon , SettingsIcon } from "lucide-react" ;
24+ import Link from "next/link" ;
25+ import { usePathname , useRouter } from "next/navigation" ;
2326import { useEffect , useRef , useState } from "react" ;
2427import { useSlate } from "slate-react" ;
2528import { Editor } from "slate" ;
@@ -39,13 +42,28 @@ interface ConnectorsMenuProps {
3942 onSelectedSearchScopesChange : ( items : SearchScope [ ] ) => void ;
4043 disabledMcpServerIds : string [ ] ;
4144 onDisabledMcpServerIdsChange : ( ids : string [ ] ) => void ;
45+ isAuthenticated : boolean ;
4246}
4347
4448interface ChatMenuMcpServer {
4549 isConnected : boolean ;
4650 isAuthExpired : boolean ;
4751}
4852
53+ export class McpServersLoadError extends Error {
54+ constructor ( public readonly serviceError : ServiceError ) {
55+ super ( "Failed to load connectors" ) ;
56+ }
57+ }
58+
59+ export function shouldRetryMcpServersLoad ( failureCount : number , error : Error ) {
60+ if ( error instanceof McpServersLoadError && error . serviceError . errorCode === ErrorCode . NOT_AUTHENTICATED ) {
61+ return false ;
62+ }
63+
64+ return failureCount < 3 ;
65+ }
66+
4967export function splitMcpServersForChatMenu < T extends ChatMenuMcpServer > ( servers : T [ ] ) {
5068 return {
5169 connectedServers : servers . filter ( ( server ) => server . isConnected || server . isAuthExpired ) ,
@@ -68,25 +86,30 @@ export const ConnectorsMenu = ({
6886 onSelectedSearchScopesChange,
6987 disabledMcpServerIds,
7088 onDisabledMcpServerIdsChange,
89+ isAuthenticated,
7190} : ConnectorsMenuProps ) => {
7291 const [ connectingServerId , setConnectingServerId ] = useState < string | null > ( null ) ;
7392 const editor = useSlate ( ) ;
7493 const hasRestoredMcpOAuthDraft = useRef ( false ) ;
7594 const isMountedRef = useRef ( false ) ;
7695 const queryClient = useQueryClient ( ) ;
7796 const router = useRouter ( ) ;
97+ const pathname = usePathname ( ) ;
7898 const { toast } = useToast ( ) ;
7999 const isOwner = useRole ( ) === OrgRole . OWNER ;
100+ const loginHref = `/login?callbackUrl=${ encodeURIComponent ( pathname ) } ` ;
80101
81- const { data : servers = [ ] , isError, isLoading, refetch } = useQuery ( {
102+ const { data : servers = [ ] , error , isError, isLoading, refetch } = useQuery ( {
82103 queryKey : mcpQueryKeys . serversWithStatus ,
83104 queryFn : async ( ) => {
84105 const result = await getMcpServersWithStatus ( ) ;
85106 if ( isServiceError ( result ) ) {
86- throw new Error ( "Failed to load connectors" ) ;
107+ throw new McpServersLoadError ( result ) ;
87108 }
88109 return result ;
89110 } ,
111+ retry : shouldRetryMcpServersLoad ,
112+ enabled : isAuthenticated ,
90113 } ) ;
91114
92115 useEffect ( ( ) => {
@@ -193,6 +216,8 @@ export const ConnectorsMenu = ({
193216
194217 const { connectedServers, connectableServers } = splitMcpServersForChatMenu ( servers ) ;
195218 const hasServers = connectedServers . length > 0 || connectableServers . length > 0 ;
219+ const isAuthenticationError = error instanceof McpServersLoadError && error . serviceError . errorCode === ErrorCode . NOT_AUTHENTICATED ;
220+ const shouldShowLoginConnectorItem = ! isAuthenticated || isAuthenticationError ;
196221
197222 return (
198223 < DropdownMenu >
@@ -212,93 +237,108 @@ export const ConnectorsMenu = ({
212237 Connectors
213238 </ DropdownMenuSubTrigger >
214239 < DropdownMenuSubContent className = "w-56" >
215- { isError && ! hasServers ? (
216- < DropdownMenuItem
217- onSelect = { ( e ) => {
218- e . preventDefault ( ) ;
219- refetch ( ) ;
220- } }
221- className = "gap-2 text-destructive"
222- >
223- < RefreshCwIcon className = "w-4 h-4" />
224- Failed to load. Retry?
225- </ DropdownMenuItem >
226- ) : isLoading ? (
227- < DropdownMenuItem disabled >
228- Loading connectors...
229- </ DropdownMenuItem >
230- ) : ! hasServers ? (
231- < DropdownMenuItem disabled >
232- No connectors available
233- </ DropdownMenuItem >
234- ) : (
240+ { ! shouldShowLoginConnectorItem && (
235241 < >
236- { connectedServers . map ( ( server ) => {
237- const isEnabled = ! server . isAuthExpired && ! disabledMcpServerIds . includes ( server . id ) ;
238- return (
239- < DropdownMenuItem
240- key = { server . id }
241- onSelect = { ( e ) => e . preventDefault ( ) }
242- disabled = { server . isAuthExpired }
243- className = "flex items-center justify-between gap-2"
244- >
245- < div className = "flex items-center gap-2 min-w-0" >
246- { server . isAuthExpired ? (
247- < AlertTriangleIcon className = "w-4 h-4 shrink-0 text-yellow-500" />
248- ) : (
249- < McpFavicon faviconUrl = { server . faviconUrl } className = "w-4 h-4 rounded-sm" />
250- ) }
251- < span className = "truncate text-sm" > { server . name } </ span >
252- </ div >
253- < Switch
254- checked = { isEnabled }
255- onCheckedChange = { ( checked ) => onToggle ( server . id , checked ) }
256- disabled = { server . isAuthExpired }
257- className = "scale-75"
258- />
259- </ DropdownMenuItem >
260- ) ;
261- } ) }
262- { connectedServers . length > 0 && connectableServers . length > 0 && < DropdownMenuSeparator /> }
263- { connectableServers . map ( ( server ) => (
242+ { isError && ! hasServers ? (
264243 < DropdownMenuItem
265- key = { server . id }
266244 onSelect = { ( e ) => {
267245 e . preventDefault ( ) ;
268- void handleConnect ( server . id ) ;
246+ refetch ( ) ;
269247 } }
270- disabled = { connectingServerId !== null }
271- className = "group flex cursor-pointer items-center justify-between gap-2"
248+ className = "gap-2 text-destructive"
272249 >
273- < div className = "flex items-center gap-2 min-w-0" >
274- < McpFavicon faviconUrl = { server . faviconUrl } className = "w-4 h-4 rounded-sm" />
275- < span className = "truncate text-sm" > { server . name } </ span >
276- </ div >
277- { connectingServerId === server . id ? (
278- < Loader2Icon className = "w-4 h-4 shrink-0 animate-spin text-muted-foreground" />
279- ) : (
280- < PlusCircleIcon className = "w-4 h-4 shrink-0 text-green-600/80 transition-colors group-focus:text-green-500 group-hover:text-green-500 dark:text-green-400/80 dark:group-focus:text-green-400 dark:group-hover:text-green-400" />
281- ) }
250+ < RefreshCwIcon className = "w-4 h-4" />
251+ Failed to load. Retry?
282252 </ DropdownMenuItem >
283- ) ) }
253+ ) : isLoading ? (
254+ < DropdownMenuItem disabled >
255+ Loading connectors...
256+ </ DropdownMenuItem >
257+ ) : ! hasServers ? (
258+ < DropdownMenuItem disabled >
259+ No connectors available
260+ </ DropdownMenuItem >
261+ ) : (
262+ < >
263+ { connectedServers . map ( ( server ) => {
264+ const isEnabled = ! server . isAuthExpired && ! disabledMcpServerIds . includes ( server . id ) ;
265+ return (
266+ < DropdownMenuItem
267+ key = { server . id }
268+ onSelect = { ( e ) => e . preventDefault ( ) }
269+ disabled = { server . isAuthExpired }
270+ className = "flex items-center justify-between gap-2"
271+ >
272+ < div className = "flex items-center gap-2 min-w-0" >
273+ { server . isAuthExpired ? (
274+ < AlertTriangleIcon className = "w-4 h-4 shrink-0 text-yellow-500" />
275+ ) : (
276+ < McpFavicon faviconUrl = { server . faviconUrl } className = "w-4 h-4 rounded-sm" />
277+ ) }
278+ < span className = "truncate text-sm" > { server . name } </ span >
279+ </ div >
280+ < Switch
281+ checked = { isEnabled }
282+ onCheckedChange = { ( checked ) => onToggle ( server . id , checked ) }
283+ disabled = { server . isAuthExpired }
284+ className = "scale-75"
285+ />
286+ </ DropdownMenuItem >
287+ ) ;
288+ } ) }
289+ { connectedServers . length > 0 && connectableServers . length > 0 && < DropdownMenuSeparator /> }
290+ { connectableServers . map ( ( server ) => (
291+ < DropdownMenuItem
292+ key = { server . id }
293+ onSelect = { ( e ) => {
294+ e . preventDefault ( ) ;
295+ void handleConnect ( server . id ) ;
296+ } }
297+ disabled = { connectingServerId !== null }
298+ className = "group flex cursor-pointer items-center justify-between gap-2"
299+ >
300+ < div className = "flex items-center gap-2 min-w-0" >
301+ < McpFavicon faviconUrl = { server . faviconUrl } className = "w-4 h-4 rounded-sm" />
302+ < span className = "truncate text-sm" > { server . name } </ span >
303+ </ div >
304+ { connectingServerId === server . id ? (
305+ < Loader2Icon className = "w-4 h-4 shrink-0 animate-spin text-muted-foreground" />
306+ ) : (
307+ < PlusCircleIcon className = "w-4 h-4 shrink-0 text-green-600/80 transition-colors group-focus:text-green-500 group-hover:text-green-500 dark:text-green-400/80 dark:group-focus:text-green-400 dark:group-hover:text-green-400" />
308+ ) }
309+ </ DropdownMenuItem >
310+ ) ) }
311+ </ >
312+ ) }
313+ < DropdownMenuSeparator />
284314 </ >
285315 ) }
286- < DropdownMenuSeparator />
287- < DropdownMenuItem
288- className = "gap-2 text-muted-foreground"
289- onSelect = { ( ) => router . push ( `/settings/accountAskAgent` ) }
290- >
291- < CableIcon className = "w-4 h-4" />
292- My connectors
293- </ DropdownMenuItem >
294- { isOwner && (
295- < DropdownMenuItem
296- className = "gap-2 text-muted-foreground"
297- onSelect = { ( ) => router . push ( `/settings/workspaceAskAgent` ) }
298- >
299- < SettingsIcon className = "w-4 h-4" />
300- Workspace connectors
316+ { shouldShowLoginConnectorItem ? (
317+ < DropdownMenuItem asChild className = "gap-2 text-link focus:text-link" >
318+ < Link href = { loginHref } >
319+ < LogInIcon className = "w-4 h-4" />
320+ Log in to use connectors
321+ </ Link >
301322 </ DropdownMenuItem >
323+ ) : (
324+ < >
325+ < DropdownMenuItem
326+ className = "gap-2 text-muted-foreground"
327+ onSelect = { ( ) => router . push ( `/settings/accountAskAgent` ) }
328+ >
329+ < CableIcon className = "w-4 h-4" />
330+ My connectors
331+ </ DropdownMenuItem >
332+ { isOwner && (
333+ < DropdownMenuItem
334+ className = "gap-2 text-muted-foreground"
335+ onSelect = { ( ) => router . push ( `/settings/workspaceAskAgent` ) }
336+ >
337+ < SettingsIcon className = "w-4 h-4" />
338+ Workspace connectors
339+ </ DropdownMenuItem >
340+ ) }
341+ </ >
302342 ) }
303343 </ DropdownMenuSubContent >
304344 </ DropdownMenuSub >
0 commit comments