@iblai/web-containers 1.56.1 → 1.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/cjs/_virtual/index2.js +2 -2
  2. package/dist/cjs/_virtual/index3.js +2 -2
  3. package/dist/cjs/components/chat/hooks/use-chat-file-drag-drop.js +13 -3
  4. package/dist/cjs/components/chat/hooks/use-chat-file-drag-drop.js.map +1 -1
  5. package/dist/cjs/components/chat/hooks/use-chat-file-upload.js +12 -16
  6. package/dist/cjs/components/chat/hooks/use-chat-file-upload.js.map +1 -1
  7. package/dist/cjs/components/chat/index.js +2 -2
  8. package/dist/cjs/components/chat/index.js.map +1 -1
  9. package/dist/cjs/components/chat/not-allowed-files-notice.js +62 -0
  10. package/dist/cjs/components/chat/not-allowed-files-notice.js.map +1 -0
  11. package/dist/cjs/components/chat/ui/button.js +4 -4
  12. package/dist/cjs/components/chat/ui/button.js.map +1 -1
  13. package/dist/cjs/components/chat-input-form/composer-plus-menu.js +94 -17
  14. package/dist/cjs/components/chat-input-form/composer-plus-menu.js.map +1 -1
  15. package/dist/cjs/components/chat-input-form/mcp-connect-explainer-dialog.js +29 -0
  16. package/dist/cjs/components/chat-input-form/mcp-connect-explainer-dialog.js.map +1 -0
  17. package/dist/cjs/components/chat-input-form/mcp-servers-menu.js +199 -0
  18. package/dist/cjs/components/chat-input-form/mcp-servers-menu.js.map +1 -0
  19. package/dist/cjs/components/chat-input-form/use-mcp-active-count.js +56 -0
  20. package/dist/cjs/components/chat-input-form/use-mcp-active-count.js.map +1 -0
  21. package/dist/cjs/components/chat-input-form.js +19 -6
  22. package/dist/cjs/components/chat-input-form.js.map +1 -1
  23. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-dialogs.js +755 -90
  24. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-dialogs.js.map +1 -1
  25. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-management-content.js +386 -984
  26. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-management-content.js.map +1 -1
  27. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/field-info.js +16 -0
  28. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/field-info.js.map +1 -0
  29. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/index.js +6 -3
  30. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/index.js.map +1 -1
  31. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/mcp-connector-helpers.js +463 -0
  32. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/mcp-connector-helpers.js.map +1 -0
  33. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/oauth-app-fields.js +329 -0
  34. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/oauth-app-fields.js.map +1 -0
  35. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/remove-connector-dialog.js +20 -0
  36. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/remove-connector-dialog.js.map +1 -0
  37. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/use-mcp-server-actions.js +744 -0
  38. package/dist/cjs/components/modals/edit-mentor-modal/tabs/mcp-tab/use-mcp-server-actions.js.map +1 -0
  39. package/dist/cjs/components/modals/user-profile-modal.js +2 -0
  40. package/dist/cjs/components/modals/user-profile-modal.js.map +1 -1
  41. package/dist/cjs/components/ui/accordion.js +40 -0
  42. package/dist/cjs/components/ui/accordion.js.map +1 -0
  43. package/dist/cjs/i18n/messages/en.json.js +165 -20
  44. package/dist/cjs/i18n/messages/en.json.js.map +1 -1
  45. package/dist/cjs/i18n/messages/es.json.js +165 -20
  46. package/dist/cjs/i18n/messages/es.json.js.map +1 -1
  47. package/dist/cjs/i18n/messages/fr.json.js +165 -20
  48. package/dist/cjs/i18n/messages/fr.json.js.map +1 -1
  49. package/dist/cjs/i18n/messages/zh.json.js +165 -20
  50. package/dist/cjs/i18n/messages/zh.json.js.map +1 -1
  51. package/dist/cjs/index.js +2 -0
  52. package/dist/cjs/index.js.map +1 -1
  53. package/dist/cjs/node_modules/.pnpm/@radix-ui_react-accordion@1.2.8_@types_react-dom@19.2.1_@types_react@19.1.17__@types_re_b91476821b9fe8f172a947f727875267/node_modules/@radix-ui/react-accordion/dist/index.js +317 -0
  54. package/dist/cjs/node_modules/.pnpm/@radix-ui_react-accordion@1.2.8_@types_react-dom@19.2.1_@types_react@19.1.17__@types_re_b91476821b9fe8f172a947f727875267/node_modules/@radix-ui/react-accordion/dist/index.js.map +1 -0
  55. package/dist/cjs/node_modules/.pnpm/@radix-ui_react-collapsible@1.1.8_@types_react-dom@19.2.1_@types_react@19.1.17__@types__d84e28844e8116e61b459b615fab358a/node_modules/@radix-ui/react-collapsible/dist/index.js +173 -0
  56. package/dist/cjs/node_modules/.pnpm/@radix-ui_react-collapsible@1.1.8_@types_react-dom@19.2.1_@types_react@19.1.17__@types__d84e28844e8116e61b459b615fab358a/node_modules/@radix-ui/react-collapsible/dist/index.js.map +1 -0
  57. package/dist/cjs/node_modules/.pnpm/@radix-ui_react-collection@1.1.4_@types_react-dom@19.2.1_@types_react@19.1.17__@types_r_7047fbfe61c39c77a2126486502acdb6/node_modules/@radix-ui/react-collection/dist/index.js +73 -0
  58. package/dist/cjs/node_modules/.pnpm/@radix-ui_react-collection@1.1.4_@types_react-dom@19.2.1_@types_react@19.1.17__@types_r_7047fbfe61c39c77a2126486502acdb6/node_modules/@radix-ui/react-collection/dist/index.js.map +1 -0
  59. package/dist/cjs/node_modules/.pnpm/css-mediaquery@0.1.2/node_modules/css-mediaquery/index.js +1 -1
  60. package/dist/cjs/node_modules/.pnpm/matchmediaquery@0.4.2/node_modules/matchmediaquery/index.js +1 -1
  61. package/dist/cjs/node_modules/.pnpm/prop-types@15.8.1/node_modules/prop-types/index.js +1 -1
  62. package/dist/cjs/web-containers/tsconfig.build.tsbuildinfo +1 -0
  63. package/dist/esm/_virtual/index2.js +2 -2
  64. package/dist/esm/_virtual/index3.js +2 -2
  65. package/dist/esm/components/chat/hooks/use-chat-file-drag-drop.js +14 -4
  66. package/dist/esm/components/chat/hooks/use-chat-file-drag-drop.js.map +1 -1
  67. package/dist/esm/components/chat/hooks/use-chat-file-upload.js +13 -17
  68. package/dist/esm/components/chat/hooks/use-chat-file-upload.js.map +1 -1
  69. package/dist/esm/components/chat/index.js +2 -2
  70. package/dist/esm/components/chat/index.js.map +1 -1
  71. package/dist/esm/components/chat/not-allowed-files-notice.js +60 -0
  72. package/dist/esm/components/chat/not-allowed-files-notice.js.map +1 -0
  73. package/dist/esm/components/chat-input-form/composer-plus-menu.js +94 -17
  74. package/dist/esm/components/chat-input-form/composer-plus-menu.js.map +1 -1
  75. package/dist/esm/components/chat-input-form/mcp-connect-explainer-dialog.js +27 -0
  76. package/dist/esm/components/chat-input-form/mcp-connect-explainer-dialog.js.map +1 -0
  77. package/dist/esm/components/chat-input-form/mcp-servers-menu.js +197 -0
  78. package/dist/esm/components/chat-input-form/mcp-servers-menu.js.map +1 -0
  79. package/dist/esm/components/chat-input-form/use-mcp-active-count.js +54 -0
  80. package/dist/esm/components/chat-input-form/use-mcp-active-count.js.map +1 -0
  81. package/dist/esm/components/chat-input-form.js +19 -6
  82. package/dist/esm/components/chat-input-form.js.map +1 -1
  83. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-dialogs.js +755 -92
  84. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-dialogs.js.map +1 -1
  85. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-management-content.js +361 -968
  86. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/connector-management-content.js.map +1 -1
  87. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/field-info.js +14 -0
  88. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/field-info.js.map +1 -0
  89. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/index.js +6 -3
  90. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/index.js.map +1 -1
  91. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/mcp-connector-helpers.js +431 -0
  92. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/mcp-connector-helpers.js.map +1 -0
  93. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/oauth-app-fields.js +313 -0
  94. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/oauth-app-fields.js.map +1 -0
  95. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/remove-connector-dialog.js +18 -0
  96. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/remove-connector-dialog.js.map +1 -0
  97. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/use-mcp-server-actions.js +742 -0
  98. package/dist/esm/components/modals/edit-mentor-modal/tabs/mcp-tab/use-mcp-server-actions.js.map +1 -0
  99. package/dist/esm/components/modals/user-profile-modal.js +2 -0
  100. package/dist/esm/components/modals/user-profile-modal.js.map +1 -1
  101. package/dist/esm/components/ui/accordion.js +16 -0
  102. package/dist/esm/components/ui/accordion.js.map +1 -0
  103. package/dist/esm/i18n/messages/en.json.js +163 -21
  104. package/dist/esm/i18n/messages/en.json.js.map +1 -1
  105. package/dist/esm/i18n/messages/es.json.js +163 -21
  106. package/dist/esm/i18n/messages/es.json.js.map +1 -1
  107. package/dist/esm/i18n/messages/fr.json.js +163 -21
  108. package/dist/esm/i18n/messages/fr.json.js.map +1 -1
  109. package/dist/esm/i18n/messages/zh.json.js +163 -21
  110. package/dist/esm/i18n/messages/zh.json.js.map +1 -1
  111. package/dist/esm/index.js +1 -0
  112. package/dist/esm/index.js.map +1 -1
  113. package/dist/esm/node_modules/.pnpm/@radix-ui_react-accordion@1.2.8_@types_react-dom@19.2.1_@types_react@19.1.17__@types_re_b91476821b9fe8f172a947f727875267/node_modules/@radix-ui/react-accordion/dist/index.js +306 -0
  114. package/dist/esm/node_modules/.pnpm/@radix-ui_react-accordion@1.2.8_@types_react-dom@19.2.1_@types_react@19.1.17__@types_re_b91476821b9fe8f172a947f727875267/node_modules/@radix-ui/react-accordion/dist/index.js.map +1 -0
  115. package/dist/esm/node_modules/.pnpm/@radix-ui_react-collapsible@1.1.8_@types_react-dom@19.2.1_@types_react@19.1.17__@types__d84e28844e8116e61b459b615fab358a/node_modules/@radix-ui/react-collapsible/dist/index.js +146 -0
  116. package/dist/esm/node_modules/.pnpm/@radix-ui_react-collapsible@1.1.8_@types_react-dom@19.2.1_@types_react@19.1.17__@types__d84e28844e8116e61b459b615fab358a/node_modules/@radix-ui/react-collapsible/dist/index.js.map +1 -0
  117. package/dist/esm/node_modules/.pnpm/@radix-ui_react-collection@1.1.4_@types_react-dom@19.2.1_@types_react@19.1.17__@types_r_7047fbfe61c39c77a2126486502acdb6/node_modules/@radix-ui/react-collection/dist/index.js +71 -0
  118. package/dist/esm/node_modules/.pnpm/@radix-ui_react-collection@1.1.4_@types_react-dom@19.2.1_@types_react@19.1.17__@types_r_7047fbfe61c39c77a2126486502acdb6/node_modules/@radix-ui/react-collection/dist/index.js.map +1 -0
  119. package/dist/esm/node_modules/.pnpm/css-mediaquery@0.1.2/node_modules/css-mediaquery/index.js +1 -1
  120. package/dist/esm/node_modules/.pnpm/matchmediaquery@0.4.2/node_modules/matchmediaquery/index.js +1 -1
  121. package/dist/esm/node_modules/.pnpm/prop-types@15.8.1/node_modules/prop-types/index.js +1 -1
  122. package/dist/esm/web-containers/tsconfig.build.tsbuildinfo +1 -0
  123. package/dist/index.d.ts +31 -2
  124. package/dist/package.json +1 -1
  125. package/package.json +3 -3
  126. package/dist/cjs/web-containers/tsconfig.tsbuildinfo +0 -1
  127. package/dist/esm/web-containers/tsconfig.tsbuildinfo +0 -1
@@ -1,350 +1,69 @@
1
- import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
- import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
3
3
  import { format } from 'date-fns';
4
+ import { toast } from 'sonner';
4
5
  import { Button } from '../../../../ui/button.js';
5
- import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../../../ui/dialog.js';
6
6
  import { Input } from '../../../../ui/input.js';
7
7
  import { Switch } from '../../../../ui/switch.js';
8
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../../../ui/tabs.js';
8
9
  import { Popover, PopoverTrigger, PopoverContent } from '../../../../ui/popover.js';
9
10
  import { Calendar as Calendar$1 } from '../../../../ui/calendar.js';
10
11
  import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../../../../ui/command.js';
11
12
  import { ConnectorDialogs } from './connector-dialogs.js';
12
- import { useLocalStorage } from '../../../../../hooks/use-local-storage.js';
13
- import { useGetMCPServersQuery, useGetMentorSettingsQuery, useGetConnectedServicesQuery, useGetMCPServerConnectionsQuery, useCreateMCPServerMutation, useUpdateMCPServerMutation, useDeleteMCPServerMutation, useEditMentorJsonMutation, useLazyStartOAuthFlowQuery, useDisconnectServiceMutation, useCreateMCPServerConnectionMutation } from '@iblai/data-layer';
14
- import { TransportEnum } from '@iblai/iblai-api';
15
- import { toast } from 'sonner';
13
+ import { canManageMcpServers, currentSignInScope, connectionForScopeExists, getTransportLabelKey, TRANSPORT_OPTIONS, getMcpConnectionState, connectorImage, normalizeTransportValue, MCP_SERVER_PERMISSION_NAME } from './mcp-connector-helpers.js';
14
+ export { MCP_RADIO_ITEM_CLASS, attachedMcpServers, checkOAuthConnectionComplete, connectionHasSignIn, createMCPServerFormData, createOAuthConnection, determineCheckConnectionAction, determineMessageEventAction, executeCheckConnectionFlowLogic, executeConnectionFoundCleanup, executeHandleMessageCallback, executeMaxPollsCleanup, executeMessageEventFlowLogic, findMCPServerConnection, handleOAuthMessageResult, mcpServerSignedIn, oauthCallbackUrl, processOAuthMessageEvent, processOAuthStorageEvent, readAttachedMcpServerIds, shouldExecuteMessageAction, validateDisconnectOAuthParams } from './mcp-connector-helpers.js';
15
+ import { RemoveConnectorDialog } from './remove-connector-dialog.js';
16
+ import { useGetMCPServersQuery, useSetMCPServerMentorAuthScopeMutation, usePartialUpdateMCPServerMutation } from '@iblai/data-layer';
16
17
  import { Spinner } from '../../../../spinner/index.js';
18
+ import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from '../../../../ui/tooltip.js';
17
19
  import { cn } from '../../../../../lib/utils.js';
18
20
  import { IblPagination } from '../../../../ibl-pagination.js';
19
- import { WithPermissions, WithFormPermissions } from '@iblai/web-utils';
20
- import { useAgentSettingsOptional } from '../../agent-settings-context.js';
21
+ import { checkRbacPermission, WithPermissions, WithFormPermissions } from '@iblai/web-utils';
21
22
  import { useT } from '../../../../../i18n/context.js';
22
23
  import '../../../../../packages/i18n/dist/index.esm.js';
24
+ import { useMcpServerActions } from './use-mcp-server-actions.js';
25
+ import Plus from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/plus.js';
26
+ import Plug from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/plug.js';
23
27
  import Calendar from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/calendar.js';
24
28
  import ChevronsUpDown from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/chevrons-up-down.js';
25
29
  import Check from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/check.js';
26
- import Plug from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/plug.js';
27
- import Plus from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/plus.js';
28
- import Unlink from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/unlink.js';
29
- import Link2 from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/link-2.js';
30
- import SquarePen from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/square-pen.js';
30
+ import Building2 from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/building-2.js';
31
+ import Bot from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/bot.js';
32
+ import UserRound from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/user-round.js';
33
+ import Settings from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/settings.js';
31
34
  import Trash2 from '../../../../../node_modules/.pnpm/lucide-react@0.528.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/trash-2.js';
32
35
 
33
- const MCP_SERVER_PERMISSION_NAME = 'server';
34
- const TRANSPORT_OPTIONS = [
35
- { value: '', label: 'All Transports' },
36
- { value: TransportEnum.SSE, label: 'SSE' },
37
- { value: TransportEnum.WEBSOCKET, label: 'WebSocket' },
38
- { value: TransportEnum.STREAMABLE_HTTP, label: 'Streamable HTTP' },
39
- ];
40
- const TRANSPORT_LABEL_KEYS = {
41
- '': 'mcpTabConnectorManagementContent.transportLabelAllTransports',
42
- [TransportEnum.SSE]: 'mcpTabConnectorManagementContent.transportLabelSSE',
43
- [TransportEnum.WEBSOCKET]: 'mcpTabConnectorManagementContent.transportLabelWebSocket',
44
- [TransportEnum.STREAMABLE_HTTP]: 'mcpTabConnectorManagementContent.transportLabelStreamableHTTP',
45
- };
46
- const getTransportLabelKey = (value) => { var _a; return (_a = TRANSPORT_LABEL_KEYS[value]) !== null && _a !== void 0 ? _a : 'mcpTabConnectorManagementContent.transportLabelStreamableHTTP'; };
47
- const getTransportLabel = (transport) => {
48
- const normalized = transport === null || transport === void 0 ? void 0 : transport.toString().toLowerCase();
49
- const option = TRANSPORT_OPTIONS.find((opt) => {
50
- const optVal = opt.value ? opt.value.toString().toLowerCase() : '';
51
- return optVal === '' ? !normalized : optVal === normalized;
52
- });
53
- return (option === null || option === void 0 ? void 0 : option.label) || (transport === null || transport === void 0 ? void 0 : transport.toString()) || 'Streamable HTTP';
54
- };
55
- const normalizeTransportValue = (transport) => {
56
- const normalized = transport === null || transport === void 0 ? void 0 : transport.toString().toLowerCase();
57
- if (normalized === TransportEnum.SSE)
58
- return TransportEnum.SSE;
59
- if (normalized === TransportEnum.WEBSOCKET)
60
- return TransportEnum.WEBSOCKET;
61
- return TransportEnum.STREAMABLE_HTTP;
62
- };
63
- /**
64
- * Finds the active MCP server connection for a given server, accounting for scope rules.
65
- * - tenant scope: any connection for this server ID means connected
66
- * - user scope: connection must match the current user
67
- * - mentor scope: connection must match the current mentor being edited
68
- */
69
- const findMCPServerConnection = (connections, serverId, currentUser, currentMentorId) => {
70
- if (!(connections === null || connections === void 0 ? void 0 : connections.length))
71
- return null;
72
- const active = connections.filter((c) => c.server === serverId && c.is_active);
73
- for (const conn of active) {
74
- if (conn.scope === 'tenant')
75
- return conn;
76
- if (conn.scope === 'user' && conn.user === currentUser)
77
- return conn;
78
- if (conn.scope === 'mentor' && conn.mentor === currentMentorId)
79
- return conn;
80
- }
81
- return null;
82
- };
83
- // Helper to create FormData for MCP server
84
- const createMCPServerFormData = (data) => {
85
- var _a;
86
- const formData = new FormData();
87
- formData.append('name', data.name);
88
- formData.append('url', data.url);
89
- formData.append('transport', data.transport);
90
- if (data.description)
91
- formData.append('description', data.description);
92
- if (data.auth_type)
93
- formData.append('auth_type', data.auth_type);
94
- if (data.auth_scope)
95
- formData.append('auth_scope', data.auth_scope);
96
- if (data.credentials)
97
- formData.append('credentials', data.credentials);
98
- if (data.image)
99
- formData.append('image', data.image);
100
- if (data.mentor !== undefined)
101
- formData.append('mentor', (_a = data.mentor) !== null && _a !== void 0 ? _a : '');
102
- return formData;
103
- };
104
- // Helper to validate and create OAuth connection
105
- const createOAuthConnection = async (params, onSuccess, onError) => {
106
- var _a;
107
- const { connectedServiceId, isCreatingConnection, createMCPServerConnection, tenantKey, username, serverId, authScope, mentorId, oauthDisplayName, refetchFeatured, refetchMy, refetchConnected, refetchMCPServerConnections, refetchMentorSettings, } = params;
108
- if (isCreatingConnection || !connectedServiceId || !Number.isFinite(connectedServiceId)) {
109
- return false;
110
- }
111
- const scope = authScope || 'user';
112
- try {
113
- await createMCPServerConnection({
114
- org: tenantKey,
115
- userId: username,
116
- server: serverId,
117
- scope,
118
- auth_type: 'oauth2',
119
- ...(scope !== 'mentor' ? { user: username } : {}),
120
- connected_service: connectedServiceId,
121
- ...(scope === 'mentor' && mentorId ? { mentor: mentorId } : {}),
122
- }).unwrap();
123
- await Promise.all([
124
- refetchFeatured(),
125
- refetchMy(),
126
- refetchConnected(),
127
- refetchMCPServerConnections(),
128
- refetchMentorSettings(),
129
- ]);
130
- toast.success(`${oauthDisplayName} connected successfully`);
131
- onSuccess();
132
- return true;
133
- }
134
- catch (error) {
135
- const err = error;
136
- toast.error(`Failed to create connection: ${((_a = err === null || err === void 0 ? void 0 : err.data) === null || _a === void 0 ? void 0 : _a.detail) || 'Unknown error'}`);
137
- onError(error);
138
- return false;
139
- }
140
- };
141
- // Helper to process OAuth storage events
142
- const processOAuthStorageEvent = (event, expectedProvider, expectedServiceName) => {
143
- if (event.key !== 'oauth_connection_complete') {
144
- return null;
145
- }
146
- try {
147
- const data = JSON.parse(event.newValue || '{}');
148
- if (data.connectedServiceId) {
149
- const isMatch = data.provider === expectedProvider && data.serviceName === expectedServiceName;
150
- return { connectedServiceId: data.connectedServiceId, isMatch };
151
- }
152
- }
153
- catch (_a) {
154
- // Invalid JSON, return null to trigger fallback
155
- }
156
- return null;
157
- };
158
- // Helper to process OAuth message events
159
- const processOAuthMessageEvent = (event, expectedOrigin, expectedProvider, expectedServiceName) => {
160
- var _a, _b;
161
- if (event.origin !== expectedOrigin) {
162
- return null;
163
- }
164
- if (((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) === 'GOOGLE_AUTH_SUCCESS' && ((_b = event.data) === null || _b === void 0 ? void 0 : _b.connectedServiceId)) {
165
- if (event.data.provider === expectedProvider &&
166
- event.data.serviceName === expectedServiceName) {
167
- return event.data.connectedServiceId;
168
- }
169
- }
170
- return null;
171
- };
172
- // Helper to check if OAuth connection is complete
173
- const checkOAuthConnectionComplete = async (refetchConnected, provider, serviceName) => {
174
- try {
175
- const updated = await refetchConnected();
176
- if (updated === null || updated === void 0 ? void 0 : updated.data) {
177
- const connService = updated.data.find((cs) => cs.provider === provider && cs.service === serviceName);
178
- if (connService === null || connService === void 0 ? void 0 : connService.id) {
179
- return connService.id;
180
- }
181
- }
182
- }
183
- catch (_a) {
184
- // Continue polling on error
185
- }
186
- return null;
187
- };
188
- // Helper to handle OAuth message event result and determine if connection should be created
189
- const handleOAuthMessageResult = (event, expectedOrigin, expectedProvider, expectedServiceName) => {
190
- const connectedServiceId = processOAuthMessageEvent(event, expectedOrigin, expectedProvider, expectedServiceName);
191
- return {
192
- shouldCreate: connectedServiceId !== null && connectedServiceId > 0,
193
- connectedServiceId,
194
- };
195
- };
196
- const validateDisconnectOAuthParams = (tenantKey, username, connectedServiceId) => {
197
- if (!tenantKey || !username) {
198
- return {
199
- isValid: false,
200
- error: 'Missing required parameters',
201
- connectedServiceId: null,
202
- };
203
- }
204
- if (!connectedServiceId) {
205
- return {
206
- isValid: false,
207
- error: 'No connected service to disconnect',
208
- connectedServiceId: null,
209
- };
210
- }
211
- return {
212
- isValid: true,
213
- error: null,
214
- connectedServiceId,
215
- };
216
- };
217
- // Determines the action to take in checkConnection based on the current state
218
- const determineCheckConnectionAction = (isCreatingConnection, connectedServiceId, createConnectionSuccess, pollCount, maxPolls) => {
219
- if (isCreatingConnection) {
220
- return { action: 'skip', connectedServiceId: null };
221
- }
222
- if (pollCount >= maxPolls) {
223
- return { action: 'max_polls_cleanup', connectedServiceId: null };
224
- }
225
- return { action: 'continue_polling', connectedServiceId };
226
- };
227
- // Determines the action to take after processing a message event
228
- const determineMessageEventAction = (shouldCreate, connectedServiceId) => {
229
- if (shouldCreate && connectedServiceId) {
230
- return { action: 'create_and_cleanup', connectedServiceId };
231
- }
232
- return { action: 'ignore', connectedServiceId: null };
233
- };
234
- // Helper to check if message event should trigger cleanup and create
235
- const shouldExecuteMessageAction = (actionResult) => {
236
- return actionResult.action === 'create_and_cleanup' && actionResult.connectedServiceId !== null;
237
- };
238
- // Execute the check connection flow logic - returns what action was taken
239
- const executeCheckConnectionFlowLogic = (connectedServiceId, createConnectionSuccess, pollCount, maxPolls) => {
240
- // Determine if we should create and cleanup
241
- if (connectedServiceId && createConnectionSuccess) {
242
- return {
243
- executed: true,
244
- action: 'create_and_cleanup',
245
- connectedServiceId,
246
- };
247
- }
248
- // Check if max polls reached
249
- if (pollCount >= maxPolls) {
250
- return {
251
- executed: true,
252
- action: 'max_polls_cleanup',
253
- connectedServiceId: null,
254
- };
255
- }
256
- return {
257
- executed: false,
258
- action: 'continue_polling',
259
- connectedServiceId: null,
260
- };
261
- };
262
- // Execute the message event flow logic - returns what action was taken
263
- const executeMessageEventFlowLogic = (event, expectedOrigin, expectedProvider, expectedServiceName) => {
264
- const { shouldCreate, connectedServiceId } = handleOAuthMessageResult(event, expectedOrigin, expectedProvider, expectedServiceName);
265
- const actionResult = determineMessageEventAction(shouldCreate, connectedServiceId);
266
- if (shouldExecuteMessageAction(actionResult)) {
267
- return {
268
- executed: true,
269
- action: 'create_and_cleanup',
270
- connectedServiceId: actionResult.connectedServiceId,
271
- };
272
- }
273
- return {
274
- executed: false,
275
- action: 'ignore',
276
- connectedServiceId: null,
277
- };
278
- };
279
- const executeHandleMessageCallback = async (params) => {
280
- const flowResult = executeMessageEventFlowLogic(params.event, params.expectedOrigin, params.provider, params.serviceName);
281
- if (flowResult.executed && flowResult.connectedServiceId) {
282
- params.cleanup();
283
- await params.doCreateConnection(flowResult.connectedServiceId);
284
- return {
285
- executed: true,
286
- connectedServiceId: flowResult.connectedServiceId,
287
- };
288
- }
289
- return { executed: false, connectedServiceId: null };
290
- };
291
- const executeMaxPollsCleanup = (params) => {
292
- const maxPollsFlowResult = executeCheckConnectionFlowLogic(null, false, params.pollCount, params.maxPolls);
293
- if (maxPollsFlowResult.executed) {
294
- params.cleanup();
295
- return true;
296
- }
297
- return false;
298
- };
299
- const executeConnectionFoundCleanup = async (params) => {
300
- const success = await params.doCreateConnection(params.connectedServiceId);
301
- const flowResult = executeCheckConnectionFlowLogic(params.connectedServiceId, success, params.pollCount, params.maxPolls);
302
- if (flowResult.executed) {
303
- params.cleanup();
304
- return { cleaned: true, success };
305
- }
306
- return { cleaned: false, success };
307
- };
308
- function ConnectorManagementContent({ tenantKey, username, mentorId, rbacPermissions, onSelect, }) {
309
- var _a, _b, _c;
36
+ // The agent settings tabs' own sub-tab look (Settings, Memory, ...).
37
+ const SUB_TAB_TRIGGER_CLASS = 'px-3 py-1.5 text-sm font-medium text-gray-700 data-[state=active]:bg-white data-[state=active]:text-gray-900';
38
+ // A connector seeded from the MCP directory: the organization cannot edit or
39
+ // delete it; the one thing an agent may change is how it signs in (guide §5.9).
40
+ const isDirectoryServer = (server) => { var _a; return ((_a = server.registry) === null || _a === void 0 ? void 0 : _a.source) === 'registry'; };
41
+ // Only a sign-in (OAuth) connector has a way of signing in to choose.
42
+ const isOAuthServer = (server) => { var _a; return ((_a = server.auth_type) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'oauth2' && !!server.oauth_service_data; };
43
+ // Featured and directory connectors cannot be edited by the organization:
44
+ // Configure opens the form with only the settings for this agent enabled.
45
+ const isLockedServer = (server) => !!server.is_featured || isDirectoryServer(server);
46
+ function ConnectorManagementContent({ tenantKey, username, mentorId, rbacPermissions, enableRbac = false, onSelect, }) {
310
47
  const t = useT();
311
- // Outside an agent-settings host (e.g. a standalone picker) there is no RBAC
312
- // flag, so permissions are not enforced, as before.
313
- const enableRBAC = (_b = (_a = useAgentSettingsOptional()) === null || _a === void 0 ? void 0 : _a.enableRBAC) !== null && _b !== void 0 ? _b : false;
314
48
  const itemsPerPage = 12;
315
- // Filter state
316
49
  const [searchQuery, setSearchQuery] = useState('');
317
50
  const [dateRange, setDateRange] = useState(undefined);
318
51
  const [selectedTransport, setSelectedTransport] = useState('');
319
- // Pagination state
52
+ const [section, setSection] = useState('featured');
320
53
  const [featuredPage, setFeaturedPage] = useState(1);
321
54
  const [myConnectorsPage, setMyConnectorsPage] = useState(1);
322
- // Dialog state
323
55
  const [showAddConnectorDialog, setShowAddConnectorDialog] = useState(false);
324
- const [deletingServerId, setDeletingServerId] = useState(null);
325
56
  const [connectorToDelete, setConnectorToDelete] = useState(null);
326
57
  const [editingServer, setEditingServer] = useState(null);
327
- const [togglingServerIds, setTogglingServerIds] = useState(() => new Set());
328
- const [connectingServiceIds, setConnectingServiceIds] = useState(() => new Set());
329
- const [disconnectingServiceIds, setDisconnectingServiceIds] = useState(() => new Set());
330
- // Use localStorage hook for pending OAuth server data
331
- const [, setPendingOAuthServer, removePendingOAuthServer] = useLocalStorage('oauth_pending_server', null);
332
- // Tracks in-flight OAuth cleanup callbacks so they fire on unmount, not just
333
- // on successful completion / timeout / popup-closed paths inside
334
- // `handleConnectOAuth`. Without this, navigating away mid-flow would leak
335
- // the `storage` / `message` / `focus` window listeners until the 5-minute
336
- // safety timer eventually fires.
337
- const oauthCleanupsRef = useRef(new Set());
338
- useEffect(() => () => {
339
- oauthCleanupsRef.current.forEach((fn) => fn());
340
- oauthCleanupsRef.current.clear();
341
- }, []);
342
- // Reset to first page when filters change
58
+ // A connector the organization cannot edit: the form opens with only its
59
+ // settings for this agent (and its sign-in app) enabled.
60
+ const [restricted, setRestricted] = useState(false);
61
+ const [openOnSignInApp, setOpenOnSignInApp] = useState(false);
62
+ const canSetUpOAuthApp = checkRbacPermission(rbacPermissions !== null && rbacPermissions !== void 0 ? rbacPermissions : {}, '/mcpservers/#create', enableRbac);
343
63
  useEffect(() => {
344
64
  setFeaturedPage(1);
345
65
  setMyConnectorsPage(1);
346
66
  }, [searchQuery, dateRange, selectedTransport]);
347
- // Build query params
348
67
  const buildQueryParams = useCallback((page, isFeatured) => ({
349
68
  org: tenantKey,
350
69
  userId: username,
@@ -356,86 +75,235 @@ function ConnectorManagementContent({ tenantKey, username, mentorId, rbacPermiss
356
75
  ...(searchQuery && { search: searchQuery }),
357
76
  ...(selectedTransport && { transport: selectedTransport }),
358
77
  }), [tenantKey, username, mentorId, searchQuery, selectedTransport]);
359
- // API hooks
360
78
  const { data: featuredServers, isLoading: isLoadingFeatured, error: featuredError, refetch: refetchFeatured, } = useGetMCPServersQuery(buildQueryParams(featuredPage, true), {
361
79
  skip: !tenantKey || !username,
362
80
  });
363
81
  const { data: myServers, isLoading: isLoadingMy, error: myError, refetch: refetchMy, } = useGetMCPServersQuery(buildQueryParams(myConnectorsPage, false), {
364
82
  skip: !tenantKey || !username,
365
83
  });
366
- const { data: mentorSettings, refetch: refetchMentorSettings } = useGetMentorSettingsQuery({
367
- mentor: mentorId,
368
- org: tenantKey,
369
- // @ts-expect-error userId is not part of useGetMentorSettingsQuery Query definition
370
- userId: username,
371
- }, { skip: !username });
372
- const { refetch: refetchConnected } = useGetConnectedServicesQuery({ org: tenantKey, userId: username }, { skip: !tenantKey || !username });
373
- const { data: mcpServerConnections, refetch: refetchMCPServerConnections } = useGetMCPServerConnectionsQuery({ org: tenantKey, userId: username }, { skip: !tenantKey || !username });
374
- // Mutations
375
- const [createMCPServer] = useCreateMCPServerMutation();
376
- const [updateMCPServer] = useUpdateMCPServerMutation();
377
- const [deleteMCPServer] = useDeleteMCPServerMutation();
378
- const [editMentorJson] = useEditMentorJsonMutation();
379
- const [startOAuthFlow] = useLazyStartOAuthFlowQuery();
380
- const [disconnectService] = useDisconnectServiceMutation();
381
- const [createMCPServerConnection] = useCreateMCPServerConnectionMutation();
382
- // Get active MCP server IDs from mentor settings
383
- const activeMcpServerIds = useMemo(() => {
384
- if (!mentorSettings || typeof mentorSettings !== 'object')
385
- return [];
386
- const maybeServers = mentorSettings.mcp_servers;
387
- if (!Array.isArray(maybeServers))
388
- return [];
389
- return maybeServers
390
- .map((entry) => {
391
- if (typeof entry === 'number' && Number.isFinite(entry))
392
- return entry;
393
- if (typeof entry === 'object' && entry !== null && 'id' in entry) {
394
- const id = entry.id;
395
- if (typeof id === 'number')
396
- return id;
84
+ const refetchServers = useCallback(() => Promise.all([refetchFeatured(), refetchMy()]), [refetchFeatured, refetchMy]);
85
+ const serverDisplayName = useCallback((server) => {
86
+ var _a;
87
+ return server.name ||
88
+ ((_a = server.oauth_service_data) === null || _a === void 0 ? void 0 : _a.display_name) ||
89
+ t('mcpTabConnectorManagementContent.unknownConnector');
90
+ }, [t]);
91
+ const { activeMcpServerIds, fieldPermissions, attachmentsKnown, isOAuthServerConnected, hasConnectionForScope, refetchMCPServerConnections, getConnectedServiceId, getSignedInServiceId, ownsConnectedService, getConnectionId, handleToggleConnector, saveConnector, deleteServer, deletingServerId, handleConnectOAuth, handleDisconnectOAuth, refetchAfterOAuth: handleOAuthComplete, togglingServerIds, connectingServiceIds, disconnectingServiceIds, } = useMcpServerActions({
92
+ tenantKey,
93
+ username,
94
+ mentorId,
95
+ refetchServers,
96
+ softUserScopeConnectFailure: true,
97
+ // No sign-in app for this organization yet: open the setup dialog for
98
+ // whoever may add connectors, instead of the error.
99
+ onConnectNotConfigured: (server) => {
100
+ var _a, _b, _c;
101
+ // Said the same way as in the chat panel, for whoever cannot set it up.
102
+ if (!canSetUpOAuthApp) {
103
+ toast.info(t('mcpServersMenu.notSetUp', { name: serverDisplayName(server) }));
104
+ return true;
397
105
  }
398
- return null;
399
- })
400
- .filter((id) => typeof id === 'number' && Number.isFinite(id));
401
- }, [mentorSettings]);
402
- // Get current tool_slugs from mentor_tools in mentor settings
403
- // mentor_tools is an array of objects like: [{ name: "MCP", slug: "mcp", ... }]
404
- const currentToolSlugs = useMemo(() => {
405
- if (!mentorSettings || typeof mentorSettings !== 'object')
406
- return [];
407
- const maybeTools = mentorSettings.mentor_tools;
408
- if (!Array.isArray(maybeTools))
409
- return [];
410
- return maybeTools
411
- .map((tool) => {
412
- if (typeof tool === 'object' && tool !== null && 'slug' in tool) {
413
- const slug = tool.slug;
414
- if (typeof slug === 'string')
415
- return slug;
106
+ const canEdit = (_c = (_b = (_a = server.permissions) === null || _a === void 0 ? void 0 : _a.object) === null || _b === void 0 ? void 0 : _b.write) !== null && _c !== void 0 ? _c : !enableRbac;
107
+ setRestricted(isLockedServer(server) || !canEdit);
108
+ setOpenOnSignInApp(true);
109
+ // Connect passes the server already carrying the choice in force for this
110
+ // agent, so the form opens on it.
111
+ setEditingServer(server);
112
+ setShowAddConnectorDialog(true);
113
+ return true;
114
+ },
115
+ });
116
+ const canManage = canManageMcpServers({ attachmentsKnown, fieldPermissions, enableRbac });
117
+ // A sign-in choice made in the form is saved at once for a connector the agent
118
+ // uses; otherwise it is kept and stored when the connector is turned on. The
119
+ // choice shown stays the chosen one until the list reflects it.
120
+ const chosenScopes = useRef(new Map());
121
+ const pendingScopes = useRef(new Map());
122
+ const [setMentorAuthScope] = useSetMCPServerMentorAuthScopeMutation();
123
+ const [patchServer] = usePartialUpdateMCPServerMutation();
124
+ // Only for a server as the list has it: on a copy that already carries the
125
+ // choice it would drop a kept choice the list never got.
126
+ const shownScope = useCallback((server) => {
127
+ const listed = currentSignInScope(server);
128
+ const chosen = chosenScopes.current.get(server.id);
129
+ if (chosen === undefined)
130
+ return listed;
131
+ // The list now reflects the choice: from here on the list is the source.
132
+ if (chosen === listed)
133
+ chosenScopes.current.delete(server.id);
134
+ return chosen;
135
+ }, []);
136
+ // Unlike shownScope, never settles a kept choice against `server`, so it is
137
+ // safe on a copy that already carries the choice.
138
+ const scopeInForce = useCallback((server) => { var _a; return (_a = chosenScopes.current.get(server.id)) !== null && _a !== void 0 ? _a : currentSignInScope(server); }, []);
139
+ const storeScope = useCallback(async (server, scope) => {
140
+ await setMentorAuthScope({
141
+ org: tenantKey,
142
+ userId: username,
143
+ id: server.id,
144
+ mentor: mentorId,
145
+ // The connector's own scope is stored as "no override".
146
+ authScope: scope === (server.auth_scope || 'user') ? null : scope,
147
+ }).unwrap();
148
+ pendingScopes.current.delete(server.id);
149
+ }, [setMentorAuthScope, tenantKey, username, mentorId]);
150
+ // The form and a sign-in use the choice in force for this agent, kept ones included.
151
+ const withShownScope = useCallback((server) => isOAuthServer(server) ? { ...server, effective_auth_scope: shownScope(server) } : server, [shownScope]);
152
+ const openEdit = useCallback((server, restrictedEdit) => {
153
+ setRestricted(restrictedEdit);
154
+ setEditingServer(withShownScope(server));
155
+ setShowAddConnectorDialog(true);
156
+ }, [withShownScope]);
157
+ // The organization edited the connector's own scope, so the agent's override
158
+ // no longer applies; best-effort, like the connection patch.
159
+ const clearAgentScopeOverride = useCallback(async (server) => {
160
+ try {
161
+ await setMentorAuthScope({
162
+ org: tenantKey,
163
+ userId: username,
164
+ id: server.id,
165
+ mentor: mentorId,
166
+ authScope: null,
167
+ }).unwrap();
168
+ }
169
+ catch (_a) {
170
+ // The connector's own scope was saved; the override clears on the next edit.
171
+ }
172
+ chosenScopes.current.delete(server.id);
173
+ pendingScopes.current.delete(server.id);
174
+ }, [setMentorAuthScope, tenantKey, username, mentorId]);
175
+ // A connector that signs in once for everyone (agent or organization) and has
176
+ // no such sign-in yet: turning it on also opens that sign-in.
177
+ const needsSharedSignIn = useCallback((server) => {
178
+ const scope = shownScope(server);
179
+ return isOAuthServer(server) && scope !== 'user' && !hasConnectionForScope(server.id, scope);
180
+ }, [shownScope, hasConnectionForScope]);
181
+ // Turning on also stores a sign-in choice made while the connector was off.
182
+ const turnOn = useCallback(async (server) => {
183
+ const name = serverDisplayName(server);
184
+ const attached = await handleToggleConnector(server.id, name, true);
185
+ if (!attached)
186
+ return;
187
+ const pending = pendingScopes.current.get(server.id);
188
+ if (pending === undefined)
189
+ return;
190
+ try {
191
+ await storeScope(server, pending);
192
+ }
193
+ catch (_a) {
194
+ toast.error(t('mcpTabConnectorManagementContent.saveFailed', { name }));
195
+ }
196
+ }, [serverDisplayName, handleToggleConnector, storeScope, t]);
197
+ // The sign-in starts in the same click, so its tab opens at once.
198
+ const turnOnAndSignIn = useCallback((server) => {
199
+ if (needsSharedSignIn(server))
200
+ void handleConnectOAuth(withShownScope(server));
201
+ return turnOn(server);
202
+ }, [needsSharedSignIn, handleConnectOAuth, withShownScope, turnOn]);
203
+ const requestToggle = useCallback((server, checked) => {
204
+ if (checked)
205
+ void turnOnAndSignIn(server);
206
+ else
207
+ void handleToggleConnector(server.id, serverDisplayName(server), false);
208
+ }, [turnOnAndSignIn, handleToggleConnector, serverDisplayName]);
209
+ // Picking closes the workflow editor's connector panel, and with it a sign-in
210
+ // still open in its tab, so a connector waiting for one is picked once it lands.
211
+ const pendingSelectRef = useRef(null);
212
+ const selectConnector = useCallback(async (server) => {
213
+ if (activeMcpServerIds.includes(server.id)) {
214
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(server);
215
+ return;
216
+ }
217
+ const waitsForSignIn = needsSharedSignIn(server);
218
+ if (waitsForSignIn)
219
+ pendingSelectRef.current = { server, scope: shownScope(server) };
220
+ await turnOnAndSignIn(server);
221
+ if (!waitsForSignIn)
222
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(server);
223
+ }, [activeMcpServerIds, needsSharedSignIn, shownScope, turnOnAndSignIn, onSelect]);
224
+ useEffect(() => {
225
+ const pending = pendingSelectRef.current;
226
+ if (!pending)
227
+ return;
228
+ if (hasConnectionForScope(pending.server.id, pending.scope)) {
229
+ pendingSelectRef.current = null;
230
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(pending.server);
231
+ return;
232
+ }
233
+ // The sign-in ended without a connection (tab closed, refused): nothing to pick.
234
+ if (!connectingServiceIds.has(`server-${pending.server.id}`))
235
+ pendingSelectRef.current = null;
236
+ }, [connectingServiceIds, hasConnectionForScope, onSelect]);
237
+ // A connector edited to sign in for everyone, while on for this agent with no
238
+ // such sign-in yet, goes off by itself until it is turned on again.
239
+ const handleAuthScopeChanged = useCallback(async (server, authScope) => {
240
+ if (authScope === 'user' || !activeMcpServerIds.includes(server.id))
241
+ return;
242
+ const { data } = await refetchMCPServerConnections();
243
+ if (connectionForScopeExists(data === null || data === void 0 ? void 0 : data.results, server.id, authScope, username, mentorId))
244
+ return;
245
+ const name = serverDisplayName(server);
246
+ await handleToggleConnector(server.id, name, false, { quiet: true });
247
+ toast.info(t('mcpTabConnectorManagementContent.turnedOffUntilConnected', { name }));
248
+ }, [
249
+ activeMcpServerIds,
250
+ refetchMCPServerConnections,
251
+ username,
252
+ mentorId,
253
+ serverDisplayName,
254
+ handleToggleConnector,
255
+ t,
256
+ ]);
257
+ // The settings for this agent of a connector the organization cannot edit,
258
+ // saved from the form's restricted mode. The failure is said here and
259
+ // rethrown, so the form stays open.
260
+ const handleRestrictedSave = useCallback(async (server, { scope, userCanDisable }) => {
261
+ const name = serverDisplayName(server);
262
+ const attached = activeMcpServerIds.includes(server.id);
263
+ const scopeChanged = isOAuthServer(server) && scope !== scopeInForce(server);
264
+ const disableChanged = userCanDisable !== (server.user_can_disable !== false);
265
+ try {
266
+ // First, since the organization may be refused it for a featured or
267
+ // directory connector: nothing else is stored when it is.
268
+ if (disableChanged) {
269
+ await patchServer({
270
+ id: server.id,
271
+ org: tenantKey,
272
+ userId: username,
273
+ body: { user_can_disable: userCanDisable },
274
+ }).unwrap();
416
275
  }
417
- return null;
418
- })
419
- .filter((slug) => typeof slug === 'string');
420
- }, [mentorSettings]);
421
- // Field-level permissions from mentor settings (for mcp_servers write check)
422
- const fieldPermissions = (_c = mentorSettings === null || mentorSettings === void 0 ? void 0 : mentorSettings.permissions) === null || _c === void 0 ? void 0 : _c.field;
423
- // OAuth helpers — use mcp-server-connections endpoint for scope-aware detection
424
- const isOAuthServerConnected = useCallback((server) => {
425
- const connection = findMCPServerConnection(mcpServerConnections === null || mcpServerConnections === void 0 ? void 0 : mcpServerConnections.results, server.id, username, mentorId);
426
- return !!connection;
427
- }, [mcpServerConnections, username, mentorId]);
428
- const getConnectedServiceId = useCallback((server) => {
429
- var _a;
430
- const connection = findMCPServerConnection(mcpServerConnections === null || mcpServerConnections === void 0 ? void 0 : mcpServerConnections.results, server.id, username, mentorId);
431
- return (_a = connection === null || connection === void 0 ? void 0 : connection.connected_service) !== null && _a !== void 0 ? _a : null;
432
- }, [mcpServerConnections, username, mentorId]);
433
- const getConnectionId = useCallback((server) => {
434
- var _a;
435
- const connection = findMCPServerConnection(mcpServerConnections === null || mcpServerConnections === void 0 ? void 0 : mcpServerConnections.results, server.id, username, mentorId);
436
- return (_a = connection === null || connection === void 0 ? void 0 : connection.id) !== null && _a !== void 0 ? _a : null;
437
- }, [mcpServerConnections, username, mentorId]);
438
- // Filter servers by date range
276
+ if (scopeChanged) {
277
+ // The override can only be stored for a connector the agent uses.
278
+ if (attached)
279
+ await storeScope(server, scope);
280
+ else
281
+ pendingScopes.current.set(server.id, scope);
282
+ chosenScopes.current.set(server.id, scope);
283
+ }
284
+ }
285
+ catch (failure) {
286
+ // The API's prose stays out of the toast: a refused change has one sentence.
287
+ const status = failure === null || failure === void 0 ? void 0 : failure.status;
288
+ toast.error(status === 403 || status === 404
289
+ ? t('mcpTabConnectorManagementContent.cannotChangeSetting', { name })
290
+ : t('mcpTabConnectorManagementContent.saveFailed', { name }));
291
+ throw failure;
292
+ }
293
+ toast.success(t('mcpTabConnectorDialogs.connectorUpdatedSuccess'));
294
+ if (scopeChanged)
295
+ await handleAuthScopeChanged(server, scope);
296
+ }, [
297
+ serverDisplayName,
298
+ activeMcpServerIds,
299
+ scopeInForce,
300
+ patchServer,
301
+ tenantKey,
302
+ username,
303
+ storeScope,
304
+ t,
305
+ handleAuthScopeChanged,
306
+ ]);
439
307
  const filterByDate = useCallback((servers) => {
440
308
  if (!(dateRange === null || dateRange === void 0 ? void 0 : dateRange.from) || !(dateRange === null || dateRange === void 0 ? void 0 : dateRange.to))
441
309
  return servers;
@@ -445,7 +313,6 @@ function ConnectorManagementContent({ tenantKey, username, mentorId, rbacPermiss
445
313
  return created >= dateRange.from && created <= dateRange.to;
446
314
  });
447
315
  }, [dateRange]);
448
- // Filtered servers
449
316
  const filteredFeaturedServers = useMemo(() => filterByDate((featuredServers === null || featuredServers === void 0 ? void 0 : featuredServers.results) || []), [featuredServers, filterByDate]);
450
317
  const featuredOAuthServers = useMemo(() => filteredFeaturedServers.filter((s) => {
451
318
  var _a;
@@ -469,600 +336,126 @@ function ConnectorManagementContent({ tenantKey, username, mentorId, rbacPermiss
469
336
  }, [myServers, filterByDate]);
470
337
  const featuredTotalPages = featuredServers ? Math.ceil(featuredServers.count / itemsPerPage) : 0;
471
338
  const myConnectorsTotalPages = myServers ? Math.ceil(myServers.count / itemsPerPage) : 0;
472
- // Update MCP servers in mentor settings with optional tool_slugs and can_use_tools
473
- const updateMCPServers = useCallback(async (serverIds, options) => {
474
- if (!mentorId)
475
- throw new Error('Invalid mentor ID');
476
- const uniqueIds = [...new Set(serverIds.filter((id) => Number.isFinite(id) && id > 0))];
477
- const requestBody = { mcp_servers: uniqueIds };
478
- // Include tool_slugs if provided
479
- if ((options === null || options === void 0 ? void 0 : options.toolSlugs) !== undefined) {
480
- requestBody.tool_slugs = options.toolSlugs;
481
- }
482
- // Always set can_use_tools to true when enabling MCP connectors
483
- if ((options === null || options === void 0 ? void 0 : options.canUseTools) !== undefined) {
484
- requestBody.can_use_tools = options.canUseTools;
485
- }
486
- await editMentorJson({
487
- mentorId,
488
- org: tenantKey,
489
- requestBody,
490
- userId: username,
491
- }).unwrap();
492
- }, [mentorId, tenantKey, username, editMentorJson]);
493
- // Handle toggle for activating/deactivating connectors
494
- const handleToggleConnector = useCallback(async (serverId, serverName, isActive) => {
495
- var _a, _b;
496
- if (!Number.isFinite(serverId) || serverId <= 0) {
497
- toast.error(t('mcpTabConnectorManagementContent.invalidServerId'));
498
- return;
499
- }
500
- setTogglingServerIds((prev) => new Set(prev).add(serverId));
501
- try {
502
- const currentIds = Array.isArray(activeMcpServerIds) ? activeMcpServerIds : [];
503
- const updatedIds = isActive
504
- ? currentIds.includes(serverId)
505
- ? currentIds
506
- : [...currentIds, serverId]
507
- : currentIds.filter((id) => id !== serverId);
508
- /* istanbul ignore next -- @preserve optimization path, tested via toggle tests */
509
- if (updatedIds.length === currentIds.length && isActive && currentIds.includes(serverId)) {
510
- return; // Already in desired state
511
- }
512
- // Prepare options for tool_slugs and can_use_tools updates
513
- const hasMcpInToolSlugs = currentToolSlugs.includes('mcp');
514
- if (isActive) {
515
- // When enabling a connector:
516
- // - If tool_slugs doesn't have 'mcp', add it
517
- // - Always set can_use_tools to true
518
- if (!hasMcpInToolSlugs) {
519
- const updatedToolSlugs = [...currentToolSlugs, 'mcp'];
520
- await updateMCPServers(updatedIds, {
521
- toolSlugs: updatedToolSlugs,
522
- canUseTools: true,
523
- });
524
- }
525
- else {
526
- // mcp already in tool_slugs, just update mcp_servers and can_use_tools
527
- await updateMCPServers(updatedIds, {
528
- canUseTools: true,
529
- });
530
- }
531
- }
532
- else {
533
- // When disabling a connector:
534
- // - If this is the last connector being disabled, remove 'mcp' from tool_slugs
535
- const isLastConnector = updatedIds.length === 0;
536
- if (isLastConnector && hasMcpInToolSlugs) {
537
- const updatedToolSlugs = currentToolSlugs.filter((slug) => slug !== 'mcp');
538
- await updateMCPServers(updatedIds, {
539
- toolSlugs: updatedToolSlugs,
540
- // Set can_use_tools based on whether there are other tools
541
- canUseTools: updatedToolSlugs.length > 0,
542
- });
543
- }
544
- else {
545
- // Not the last connector, just update mcp_servers
546
- await updateMCPServers(updatedIds);
547
- }
548
- }
549
- await refetchMentorSettings();
550
- toast.success(isActive
551
- ? t('mcpTabConnectorManagementContent.serverActivated', { serverName })
552
- : t('mcpTabConnectorManagementContent.serverDeactivated', { serverName }));
553
- }
554
- catch (error) {
555
- const err = error;
556
- const msg = ((_a = err === null || err === void 0 ? void 0 : err.data) === null || _a === void 0 ? void 0 : _a.detail) || ((_b = err === null || err === void 0 ? void 0 : err.data) === null || _b === void 0 ? void 0 : _b.error) || (err === null || err === void 0 ? void 0 : err.message) || '';
557
- if (msg.includes('does not exist') || msg.includes('not accessible')) {
558
- toast.error(t('mcpTabConnectorManagementContent.serverNotAccessible', { serverName }));
559
- }
560
- else {
561
- toast.error(isActive
562
- ? t('mcpTabConnectorManagementContent.failedToActivate', { serverName, msg })
563
- : t('mcpTabConnectorManagementContent.failedToDeactivate', { serverName, msg }));
564
- }
565
- }
566
- finally {
567
- setTogglingServerIds((prev) => {
568
- const next = new Set(prev);
569
- next.delete(serverId);
570
- return next;
571
- });
572
- }
573
- }, [activeMcpServerIds, currentToolSlugs, updateMCPServers, refetchMentorSettings]);
574
- // Handle add/edit connector
575
339
  const handleAddConnectorClick = useCallback(async (connector) => {
576
- var _a, _b;
577
- if (!tenantKey || !username) {
578
- toast.error(t('mcpTabConnectorManagementContent.missingRequiredParams'));
579
- return;
580
- }
581
- try {
582
- const hasFileImage = connector.image instanceof File;
583
- const trimmedCredentials = (_a = connector.credentials) === null || _a === void 0 ? void 0 : _a.trim();
584
- const authType = (_b = connector.authType) !== null && _b !== void 0 ? _b : (trimmedCredentials ? 'token' : 'none');
585
- const resolvedTransport = normalizeTransportValue(connector.transport);
586
- const basePayload = {
587
- name: connector.name,
588
- url: connector.url || `https://api.${connector.name.toLowerCase()}.com/mcp`,
589
- transport: resolvedTransport,
590
- auth_type: authType,
591
- description: connector.description || '',
592
- // Only include credentials if provided (for edits, undefined means keep existing)
593
- ...(trimmedCredentials !== undefined && {
594
- credentials: trimmedCredentials,
595
- }),
596
- ...(connector.authScope && { auth_scope: connector.authScope }),
597
- ...(connector.mentor !== undefined && { mentor: connector.mentor }),
598
- };
599
- /* istanbul ignore next -- @preserve update path tested via ConnectorDialogs integration */
600
- if (editingServer) {
601
- // Update existing server
602
- const args = {
603
- id: editingServer.id,
604
- org: tenantKey,
605
- userId: username,
606
- ...(hasFileImage
607
- ? {
608
- formData: createMCPServerFormData({
609
- ...basePayload,
610
- image: connector.image,
611
- }),
612
- }
613
- : { body: basePayload }),
614
- };
615
- await updateMCPServer(args).unwrap();
616
- toast.success(t('mcpTabConnectorManagementContent.connectorUpdated', { name: connector.name }));
617
- }
618
- else {
619
- // Create new server
620
- const args = {
621
- org: tenantKey,
622
- userId: username,
623
- ...(hasFileImage
624
- ? {
625
- formData: createMCPServerFormData({
626
- ...basePayload,
627
- image: connector.image,
628
- }),
629
- }
630
- : { body: basePayload }),
631
- };
632
- const newServer = await createMCPServer(args).unwrap();
633
- toast.success(t('mcpTabConnectorManagementContent.connectorAdded', { name: connector.name }));
634
- // Auto-activate new connector
635
- try {
636
- const newId = Number(newServer.id);
637
- if (Number.isFinite(newId)) {
638
- const updatedIds = [...activeMcpServerIds, newId];
639
- const hasMcpInToolSlugs = currentToolSlugs.includes('mcp');
640
- // When auto-activating, also add 'mcp' to tool_slugs if not present
641
- if (!hasMcpInToolSlugs) {
642
- const updatedToolSlugs = [...currentToolSlugs, 'mcp'];
643
- await updateMCPServers(updatedIds, {
644
- toolSlugs: updatedToolSlugs,
645
- canUseTools: true,
646
- });
647
- }
648
- else {
649
- await updateMCPServers(updatedIds, {
650
- canUseTools: true,
651
- });
652
- }
653
- await refetchMentorSettings();
654
- }
655
- }
656
- catch (_c) {
657
- toast.warning(t('mcpTabConnectorManagementContent.connectorNotActivated', { name: connector.name }));
658
- }
659
- }
660
- setShowAddConnectorDialog(false);
661
- setEditingServer(null);
662
- await Promise.all([refetchFeatured(), refetchMy()]);
663
- }
664
- catch (_d) {
665
- toast.error(editingServer
666
- ? t('mcpTabConnectorManagementContent.failedToUpdateConnector', {
667
- name: connector.name,
668
- })
669
- : t('mcpTabConnectorManagementContent.failedToAddConnector', { name: connector.name }));
670
- }
671
- }, [
672
- tenantKey,
673
- username,
674
- editingServer,
675
- activeMcpServerIds,
676
- currentToolSlugs,
677
- createMCPServer,
678
- updateMCPServer,
679
- updateMCPServers,
680
- refetchFeatured,
681
- refetchMy,
682
- refetchMentorSettings,
683
- ]);
684
- // Handle delete connector
340
+ await saveConnector(connector, {
341
+ editingServer,
342
+ onSaved: () => {
343
+ setShowAddConnectorDialog(false);
344
+ setEditingServer(null);
345
+ },
346
+ });
347
+ }, [saveConnector, editingServer]);
685
348
  const handleDeleteConnector = useCallback(async (serverId, serverName) => {
686
- if (!tenantKey || !username) {
687
- toast.error(t('mcpTabConnectorManagementContent.missingRequiredParams'));
688
- return;
689
- }
690
- setDeletingServerId(serverId);
691
- try {
692
- await deleteMCPServer({
693
- id: serverId,
694
- org: tenantKey,
695
- userId: username,
696
- }).unwrap();
697
- toast.success(t('mcpTabConnectorManagementContent.connectorRemoved', { serverName }));
698
- await Promise.all([refetchFeatured(), refetchMy()]);
699
- try {
700
- const updatedIds = activeMcpServerIds.filter((id) => id !== serverId);
701
- await updateMCPServers(updatedIds);
702
- await refetchMentorSettings();
703
- }
704
- catch (_a) {
705
- toast.warning(t('mcpTabConnectorManagementContent.connectorDeletedStillActive', { serverName }));
706
- }
707
- }
708
- catch (_b) {
709
- toast.error(t('mcpTabConnectorManagementContent.failedToRemoveConnector', { serverName }));
710
- }
711
- finally {
712
- setDeletingServerId(null);
713
- setConnectorToDelete(null);
714
- }
715
- }, [
716
- tenantKey,
717
- username,
718
- activeMcpServerIds,
719
- deleteMCPServer,
720
- updateMCPServers,
721
- refetchFeatured,
722
- refetchMy,
723
- refetchMentorSettings,
724
- ]);
725
- // Handle OAuth connect
726
- const handleConnectOAuth = useCallback(async (server) => {
727
- if (!tenantKey || !username || !server.oauth_service_data) {
728
- toast.error(t('mcpTabConnectorManagementContent.missingOAuthParams'));
729
- return;
730
- }
731
- const oauthData = server.oauth_service_data;
732
- const { oauth_provider: provider, name } = oauthData;
733
- const serverId = server.id;
734
- const serviceKey = `server-${serverId}`;
735
- const oauthDisplayName = oauthData.display_name || name;
736
- setPendingOAuthServer({
737
- serverId,
738
- provider,
739
- service: name,
740
- timestamp: Date.now(),
349
+ await deleteServer(serverId, serverName);
350
+ setConnectorToDelete(null);
351
+ }, [deleteServer]);
352
+ // Read as is: `editingServer` already carries the scope in force (openEdit),
353
+ // and shownScope on such a copy would settle a kept choice against itself.
354
+ const describeConnection = (server) => {
355
+ const { isConnected, showConnect, showDisconnect, disconnectOptions } = getMcpConnectionState(server, {
356
+ canManage,
357
+ attachmentsKnown,
358
+ activeMcpServerIds,
359
+ isOAuthServerConnected,
360
+ getConnectedServiceId,
361
+ getSignedInServiceId,
362
+ ownsConnectedService,
741
363
  });
742
- setConnectingServiceIds((prev) => new Set(prev).add(serviceKey));
743
- // State the cleanup closure references. Declared before the first
744
- // `await` so we can register cleanup synchronously — guaranteeing an
745
- // unmount mid-flow tears down listeners, intervals, and timers.
746
- let pollCount = 0;
747
- const maxPolls = 60;
748
- let pollInterval = null;
749
- let popupCheckInterval = null;
750
- let isCreatingConnection = false;
751
- // Listener handlers — reassigned with real implementations once the
752
- // OAuth URL resolves. Default noops let cleanup safely call
753
- // removeEventListener even if we never reached addEventListener (e.g.
754
- // unmount before startOAuthFlow resolves).
755
- let handleFocus = () => { };
756
- let handleStorageChange = () => { };
757
- let handleMessage = () => { };
758
- // Cleanup function to remove listeners and reset state. Idempotent —
759
- // tracked in `oauthCleanupsRef` so component unmount can also drive it.
760
- let cleanedUp = false;
761
- const cleanup = () => {
762
- if (cleanedUp)
763
- return;
764
- cleanedUp = true;
765
- oauthCleanupsRef.current.delete(cleanup);
766
- if (pollInterval)
767
- clearInterval(pollInterval);
768
- if (popupCheckInterval)
769
- clearInterval(popupCheckInterval);
770
- window.removeEventListener('focus', handleFocus);
771
- window.removeEventListener('storage', handleStorageChange);
772
- window.removeEventListener('message', handleMessage);
773
- setConnectingServiceIds((prev) => {
774
- const next = new Set(prev);
775
- next.delete(serviceKey);
776
- return next;
777
- });
778
- removePendingOAuthServer();
779
- isCreatingConnection = false;
364
+ return {
365
+ connected: isConnected,
366
+ canConnect: showConnect,
367
+ canDisconnect: showDisconnect,
368
+ connecting: connectingServiceIds.has(`server-${server.id}`),
369
+ disconnecting: disconnectingServiceIds.has(server.id),
370
+ onConnect: () => void handleConnectOAuth(server),
371
+ onDisconnect: () => void handleDisconnectOAuth(server, disconnectOptions),
780
372
  };
781
- oauthCleanupsRef.current.add(cleanup);
782
- try {
783
- const result = await startOAuthFlow({
784
- org: tenantKey,
785
- userId: username,
786
- provider,
787
- service: name,
788
- }).unwrap();
789
- if (!result.auth_url)
790
- throw new Error('No authorization URL returned');
791
- // Bail if the flow was already torn down while we awaited the auth URL
792
- // (e.g. the component unmounted). `cleanup` has run and been dropped
793
- // from `oauthCleanupsRef`, so anything registered past this point —
794
- // listeners, intervals, timers — would leak with nothing left to
795
- // remove it.
796
- if (cleanedUp)
797
- return;
798
- const popup = window.open(result.auth_url, '_blank');
799
- // Create connection using helper function
800
- const doCreateConnection = async (connectedServiceId) => {
801
- const result = await createOAuthConnection({
802
- connectedServiceId,
803
- isCreatingConnection,
804
- createMCPServerConnection,
805
- tenantKey,
806
- username,
807
- serverId,
808
- authScope: server.auth_scope,
809
- mentorId,
810
- oauthDisplayName,
811
- refetchFeatured,
812
- refetchMy,
813
- refetchConnected,
814
- refetchMCPServerConnections,
815
- refetchMentorSettings,
816
- },
817
- /* istanbul ignore next -- @preserve success callback, tested via createOAuthConnection */
818
- () => {
819
- isCreatingConnection = false;
820
- },
821
- /* istanbul ignore next -- @preserve error callback, tested via createOAuthConnection */
822
- () => {
823
- isCreatingConnection = false;
824
- });
825
- if (result) {
826
- isCreatingConnection = true;
827
- }
828
- return result;
829
- };
830
- // Check for OAuth connection using helper
831
- const checkConnection = async () => {
832
- // Early check using helper
833
- const earlyCheck = determineCheckConnectionAction(isCreatingConnection, null, false, pollCount, maxPolls);
834
- if (earlyCheck.action === 'skip')
835
- return;
836
- try {
837
- const connectedServiceId = await checkOAuthConnectionComplete(refetchConnected, provider, name);
838
- // If we found a connected service, try to create connection using helper
839
- // Logic tested via executeConnectionFoundCleanup unit tests
840
- /* istanbul ignore next -- @preserve callback wrapper, logic tested via helper */
841
- if (connectedServiceId) {
842
- const result = await executeConnectionFoundCleanup({
843
- connectedServiceId,
844
- doCreateConnection,
845
- pollCount,
846
- maxPolls,
847
- cleanup,
848
- });
849
- if (result.cleaned) {
850
- return;
851
- }
852
- }
853
- await Promise.all([refetchFeatured(), refetchMy()]);
854
- pollCount++;
855
- // Check if max polls reached using extracted helper function
856
- executeMaxPollsCleanup({ pollCount, maxPolls, cleanup });
857
- }
858
- catch (_a) {
859
- // Continue polling
860
- }
861
- };
862
- handleFocus = () => checkConnection();
863
- // Handle OAuth message events using extracted helper function
864
- // Logic tested via executeHandleMessageCallback unit tests
865
- /* istanbul ignore next -- @preserve callback wrapper, logic tested via helper */
866
- handleMessage = async (event) => {
867
- await executeHandleMessageCallback({
868
- event,
869
- expectedOrigin: window.location.origin,
870
- provider,
871
- serviceName: name,
872
- cleanup,
873
- doCreateConnection,
874
- });
875
- };
876
- // Handle OAuth storage events using helper
877
- handleStorageChange = async (e) => {
878
- const result = processOAuthStorageEvent(e, provider, name);
879
- if (result) {
880
- if (result.isMatch) {
881
- cleanup();
882
- await doCreateConnection(result.connectedServiceId);
883
- }
884
- else {
885
- await checkConnection();
886
- }
887
- }
888
- else if (e.key === 'oauth_connection_complete') {
889
- /* istanbul ignore next */ // Fallback for parse errors or missing data
890
- await checkConnection();
891
- }
892
- };
893
- window.addEventListener('storage', handleStorageChange);
894
- window.addEventListener('message', handleMessage);
895
- window.addEventListener('focus', handleFocus);
896
- pollInterval = setInterval(checkConnection, 5000);
897
- setTimeout(cleanup, 5 * 60 * 1000);
898
- // Detect if user closed the OAuth popup without completing
899
- if (popup) {
900
- popupCheckInterval = setInterval(() => {
901
- if (popup.closed) {
902
- if (popupCheckInterval)
903
- clearInterval(popupCheckInterval);
904
- popupCheckInterval = null;
905
- // Grace period for OAuth callback/redirect to complete
906
- setTimeout(() => {
907
- if (!isCreatingConnection) {
908
- cleanup();
909
- }
910
- }, 10000);
911
- }
912
- }, 2000);
913
- }
914
- } /* istanbul ignore next */
915
- catch (_a) {
916
- toast.error(t('mcpTabConnectorManagementContent.failedToConnect', {
917
- name: oauthData.display_name || name,
918
- }));
919
- setConnectingServiceIds((prev) => {
920
- const next = new Set(prev);
921
- next.delete(serviceKey);
922
- return next;
923
- });
924
- removePendingOAuthServer();
925
- }
926
- }, [
927
- tenantKey,
928
- username,
929
- startOAuthFlow,
930
- createMCPServerConnection,
931
- refetchFeatured,
932
- refetchMy,
933
- refetchConnected,
934
- refetchMCPServerConnections,
935
- refetchMentorSettings,
936
- mentorId,
937
- setPendingOAuthServer,
938
- removePendingOAuthServer,
939
- ]);
940
- // Handle OAuth disconnect
941
- const handleDisconnectOAuth = useCallback(async (server) => {
942
- const serverConnectedServiceId = getConnectedServiceId(server);
943
- const validation = validateDisconnectOAuthParams(tenantKey, username, serverConnectedServiceId);
944
- /* istanbul ignore next -- @preserve validation error branch */
945
- if (!validation.isValid) {
946
- toast.error(validation.error || 'Validation failed');
947
- return;
948
- }
949
- const connectedServiceId = validation.connectedServiceId;
950
- setDisconnectingServiceIds((prev) => new Set(prev).add(server.id));
951
- try {
952
- const updatedIds = activeMcpServerIds.filter((id) => id !== server.id);
953
- await updateMCPServers(updatedIds);
954
- await disconnectService({
955
- org: tenantKey,
956
- userId: username,
957
- id: connectedServiceId,
958
- }).unwrap();
959
- await Promise.all([
960
- refetchFeatured(),
961
- refetchMy(),
962
- refetchConnected(),
963
- refetchMCPServerConnections(),
964
- refetchMentorSettings(),
965
- ]);
966
- toast.success(t('mcpTabConnectorManagementContent.serviceDisconnected'));
967
- }
968
- catch (_a) {
969
- toast.error(t('mcpTabConnectorManagementContent.failedToDisconnect'));
970
- }
971
- finally {
972
- setDisconnectingServiceIds((prev) => {
973
- const next = new Set(prev);
974
- next.delete(server.id);
975
- return next;
976
- });
977
- }
978
- }, [
979
- tenantKey,
980
- username,
981
- activeMcpServerIds,
982
- getConnectedServiceId,
983
- updateMCPServers,
984
- disconnectService,
985
- refetchFeatured,
986
- refetchMy,
987
- refetchConnected,
988
- refetchMCPServerConnections,
989
- refetchMentorSettings,
990
- ]);
991
- // Handle OAuth completion from ConnectorDialogs
992
- const handleOAuthComplete = useCallback(async () => {
993
- await Promise.all([
994
- refetchFeatured(),
995
- refetchMy(),
996
- refetchMCPServerConnections(),
997
- refetchMentorSettings(),
998
- ]);
999
- }, [refetchFeatured, refetchMy, refetchMCPServerConnections, refetchMentorSettings]);
1000
- // Render server card
373
+ };
374
+ const editingConnection = editingServer ? describeConnection(editingServer) : null;
375
+ // Read-only: the Authentication Scope in force for this agent, changed in Configure.
376
+ const renderScopePill = (server, displayName) => {
377
+ const scope = shownScope(server);
378
+ const ScopeIcon = scope === 'platform' ? Building2 : scope === 'mentor' ? Bot : UserRound;
379
+ const scopeName = t(scope === 'platform'
380
+ ? 'mcpTabConnectorManagementContent.scopeTenant'
381
+ : scope === 'mentor'
382
+ ? 'mcpTabConnectorManagementContent.scopeAgent'
383
+ : 'mcpTabConnectorManagementContent.scopeUser');
384
+ const scopeHint = t(scope === 'platform'
385
+ ? 'mcpTabConnectorManagementContent.scopeHintOrganization'
386
+ : scope === 'mentor'
387
+ ? 'mcpTabConnectorManagementContent.scopeHintAgent'
388
+ : 'mcpTabConnectorManagementContent.scopeHintUser', { name: displayName });
389
+ const scopeLabel = t('mcpTabConnectorDialogs.authScopeLabel');
390
+ return (jsx(TooltipProvider, { delayDuration: 200, children: jsxs(Tooltip, { children: [jsx(TooltipTrigger, { asChild: true, children: jsxs("span", { tabIndex: 0, "data-testid": `mcp-connector-scope-${server.id}`, className: "inline-flex cursor-default items-center gap-1 rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600", children: [jsxs("span", { className: "sr-only", children: [scopeLabel, ": "] }), jsx(ScopeIcon, { className: "h-3 w-3", "aria-hidden": "true" }), scopeName] }) }), jsxs(TooltipContent, { className: "ibl-tooltip-content z-50 max-w-[300px]", children: [jsx("p", { className: "font-medium", children: scopeLabel }), jsx("p", { children: scopeHint })] })] }) }));
391
+ };
392
+ const renderIconAction = ({ label, icon: Icon, testId, onClick, disabled, className, }) => (jsx(TooltipProvider, { delayDuration: 200, children: jsxs(Tooltip, { children: [jsx(TooltipTrigger, { asChild: true, children: jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": label, "data-testid": testId, className: cn('h-8 w-8 text-gray-500', className), onClick: onClick, disabled: disabled, children: jsx(Icon, { className: "h-4 w-4", "aria-hidden": "true" }) }) }), jsx(TooltipContent, { className: "ibl-tooltip-content z-50", children: label })] }) }));
1001
393
  const renderServerCard = (server, isFeatured) => {
1002
394
  var _a, _b, _c, _d, _e, _f, _g;
1003
395
  const oauthData = server.oauth_service_data;
1004
- const displayName = server.name ||
1005
- (oauthData === null || oauthData === void 0 ? void 0 : oauthData.display_name) ||
1006
- t('mcpTabConnectorManagementContent.unknownConnector');
1007
- const authType = (_a = server.auth_type) === null || _a === void 0 ? void 0 : _a.toLowerCase();
1008
- const isOAuth2 = authType === 'oauth2';
1009
- const isToken = authType === 'token';
1010
- const isConnected = isOAuth2 && isOAuthServerConnected(server);
1011
- const connectedServiceId = getConnectedServiceId(server);
1012
- const serviceKey = `server-${server.id}`;
1013
- const isConnecting = connectingServiceIds.has(serviceKey);
1014
- const isDisconnecting = disconnectingServiceIds.has(server.id);
396
+ const displayName = serverDisplayName(server);
1015
397
  const isActive = activeMcpServerIds.includes(server.id);
1016
- // Only OAuth2 needs connection flow - token auth works without connection
1017
- const needsOAuthConnection = isOAuth2 && !isConnected;
398
+ const locked = isFeatured || isLockedServer(server);
399
+ const description = (oauthData === null || oauthData === void 0 ? void 0 : oauthData.description) || server.description || server.url;
1018
400
  const objectPermissions = {
1019
- write: (_d = (_c = (_b = server.permissions) === null || _b === void 0 ? void 0 : _b.object) === null || _c === void 0 ? void 0 : _c.write) !== null && _d !== void 0 ? _d : true,
1020
- delete: (_g = (_f = (_e = server.permissions) === null || _e === void 0 ? void 0 : _e.object) === null || _f === void 0 ? void 0 : _f.delete) !== null && _g !== void 0 ? _g : true,
401
+ write: (_c = (_b = (_a = server.permissions) === null || _a === void 0 ? void 0 : _a.object) === null || _b === void 0 ? void 0 : _b.write) !== null && _c !== void 0 ? _c : true,
402
+ delete: (_f = (_e = (_d = server.permissions) === null || _d === void 0 ? void 0 : _d.object) === null || _e === void 0 ? void 0 : _e.delete) !== null && _f !== void 0 ? _f : true,
1021
403
  };
1022
- return (jsxs("div", { className: "flex flex-col overflow-hidden rounded-lg border border-gray-200 bg-gray-50", children: [jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-gray-200 p-4", children: [jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: [jsx("div", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg border border-gray-100 bg-white", children: (oauthData === null || oauthData === void 0 ? void 0 : oauthData.image) || server.image ? (jsx("img", { src: (oauthData === null || oauthData === void 0 ? void 0 : oauthData.image) || server.image, alt: displayName, className: "h-6 w-6 object-contain" })) : (jsx(Plug, { className: "h-5 w-5 text-gray-400" })) }), jsx("h4", { className: "truncate text-sm font-medium text-gray-900", children: displayName })] }), jsx("div", { className: "flex flex-shrink-0 items-center gap-2", children: needsOAuthConnection ? (jsx("span", { className: "whitespace-nowrap text-xs text-gray-600", children: t('mcpTabConnectorManagementContent.notConnected') })) : (jsx(WithFormPermissions, { name: "mcp_servers", permissions: fieldPermissions !== null && fieldPermissions !== void 0 ? fieldPermissions : {}, enableRBAC: enableRBAC, children: ({ disabled }) => disabled ? null : (jsxs(Fragment, { children: [jsx("span", { className: "whitespace-nowrap text-xs text-gray-600", children: isActive
1023
- ? t('mcpTabConnectorManagementContent.active')
1024
- : t('mcpTabConnectorManagementContent.inactive') }), jsx(Switch, { checked: isActive, onCheckedChange: (checked) => handleToggleConnector(server.id, displayName, checked), disabled: togglingServerIds.has(server.id), "aria-label": isActive
404
+ // A connector the organization cannot edit still has settings for this
405
+ // agent, so Configure opens the form with only those enabled, for whoever
406
+ // may change what the agent uses.
407
+ const restrictedEdit = locked || !objectPermissions.write;
408
+ const configureButton = renderIconAction({
409
+ label: t('mcpTabConnectorManagementContent.configure'),
410
+ icon: Settings,
411
+ testId: `mcp-connector-configure-${server.id}`,
412
+ onClick: () => openEdit(server, restrictedEdit),
413
+ className: 'hover:text-gray-700',
414
+ });
415
+ // The card's actions are icons because its on/off is a switch: one style per
416
+ // card, never a switch beside words (#2725 review). The name wraps, never truncates.
417
+ return (jsxs("div", { "data-testid": `mcp-connector-${server.id}`, role: "group", "aria-label": displayName, className: "flex flex-col gap-3 rounded-lg border p-4", children: [jsxs("div", { className: "flex items-start justify-between gap-3", children: [jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [jsx("div", { className: "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg border border-gray-100 bg-white", children: connectorImage(server) ? (jsx("img", { src: connectorImage(server), alt: displayName, className: "h-6 w-6 object-contain" })) : (jsx(Plug, { className: "h-5 w-5 text-gray-400" })) }), jsxs("div", { className: "min-w-0", children: [jsx("h4", { className: "text-sm font-medium break-words text-gray-900", children: displayName }), (isOAuthServer(server) || !isFeatured) && (jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-1", children: [isOAuthServer(server) && renderScopePill(server, displayName), !isFeatured && (
418
+ // The same word the transport filter above uses; no transport means Streamable HTTP.
419
+ jsx("span", { className: "inline-block rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-600", children: t(getTransportLabelKey(normalizeTransportValue((_g = server.transport) !== null && _g !== void 0 ? _g : undefined))) }))] }))] })] }), jsxs("div", { className: "flex flex-shrink-0 items-center gap-1", children: [!locked && (jsx(WithFormPermissions, { name: MCP_SERVER_PERMISSION_NAME, permissions: { [MCP_SERVER_PERMISSION_NAME]: objectPermissions }, enableRBAC: enableRbac, children: ({ canDelete }) => canDelete
420
+ ? renderIconAction({
421
+ label: t('mcpTabConnectorManagementContent.delete'),
422
+ icon: Trash2,
423
+ testId: `mcp-connector-delete-${server.id}`,
424
+ onClick: () => setConnectorToDelete({ id: server.id, name: server.name }),
425
+ disabled: deletingServerId === server.id,
426
+ className: 'hover:bg-red-50 hover:text-red-600',
427
+ })
428
+ : null })), restrictedEdit ? (jsx(WithFormPermissions, { name: "mcp_servers", permissions: fieldPermissions !== null && fieldPermissions !== void 0 ? fieldPermissions : {}, enableRBAC: enableRbac, children: ({ disabled }) => (disabled ? null : configureButton) })) : (jsx(WithFormPermissions, { name: MCP_SERVER_PERMISSION_NAME, permissions: { [MCP_SERVER_PERMISSION_NAME]: objectPermissions }, enableRBAC: enableRbac, children: ({ disabled }) => (disabled ? null : configureButton) })), jsx(WithFormPermissions, { name: "mcp_servers", permissions: fieldPermissions !== null && fieldPermissions !== void 0 ? fieldPermissions : {}, enableRBAC: enableRbac, children: ({ disabled }) => disabled ? null : (
429
+ // The state reads on hover (title) and to screen readers (aria-label).
430
+ jsx("span", { className: "flex flex-shrink-0 items-center", title: isActive
431
+ ? t('mcpTabConnectorManagementContent.onForThisAgent')
432
+ : t('mcpTabConnectorManagementContent.offForThisAgent'), children: jsx(Switch, { "data-testid": `mcp-connector-toggle-${server.id}`, checked: isActive, onCheckedChange: (checked) => requestToggle(server, checked), disabled: togglingServerIds.has(server.id), "aria-label": isActive
1025
433
  ? t('mcpTabConnectorManagementContent.ariaEnabled', { displayName })
1026
- : t('mcpTabConnectorManagementContent.ariaDisabled', { displayName }) })] })) })) })] }), jsxs("div", { className: "flex flex-1 flex-col p-4", children: [jsx("div", { className: "mb-3 max-h-20 overflow-y-auto text-sm text-gray-600", children: (oauthData === null || oauthData === void 0 ? void 0 : oauthData.description) || server.description || server.url }), jsxs("div", { className: "mb-3 flex flex-wrap items-center gap-2", children: [isOAuth2 && (jsx("span", { className: "rounded bg-blue-100 px-2 py-1 text-xs text-blue-600", children: "OAuth" })), isOAuth2 && server.auth_scope && (jsx("span", { className: "rounded bg-amber-100 px-2 py-1 text-xs text-amber-700", children: server.auth_scope === 'user'
1027
- ? t('mcpTabConnectorManagementContent.scopeUser')
1028
- : server.auth_scope === 'mentor'
1029
- ? t('mcpTabConnectorManagementContent.scopeAgent')
1030
- : t('mcpTabConnectorManagementContent.scopeTenant') })), isToken && (jsx("span", { className: "rounded bg-purple-100 px-2 py-1 text-xs text-purple-600", children: "Token" })), isFeatured && (oauthData === null || oauthData === void 0 ? void 0 : oauthData.oauth_provider) && (jsx("span", { className: "rounded bg-gray-100 px-2 py-1 text-xs text-gray-600", children: oauthData.oauth_provider })), !isFeatured && (jsx("span", { className: "rounded bg-gray-100 px-2 py-1 text-xs text-gray-600", children: getTransportLabel(server.transport) }))] }), jsx("div", { className: "mt-auto", children: !isFeatured ? (
1031
- // Custom (non-featured) servers: Show Edit/Delete based on object permissions, and Connect/Disconnect if OAuth
1032
- jsxs("div", { className: "space-y-2", children: [isOAuth2 && (jsx(Fragment, { children: isConnected && connectedServiceId ? (jsx(Button, { variant: "outline", size: "sm", className: "h-8 w-full text-red-600 hover:bg-red-50 hover:text-red-700", onClick: () => handleDisconnectOAuth(server), disabled: isDisconnecting, children: isDisconnecting ? (jsxs(Fragment, { children: [jsx(Spinner, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.disconnecting')] })) : (jsxs(Fragment, { children: [jsx(Unlink, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.disconnect')] })) })) : /* istanbul ignore next -- @preserve JSX branch */ needsOAuthConnection ? (jsx(Button, { variant: "outline", size: "sm", className: "h-8 w-full border-[#38A1E5] bg-[#38A1E5] text-white hover:bg-[#2d8bc7] hover:text-white", onClick: () => handleConnectOAuth(server), disabled: isConnecting, children: isConnecting ? (jsxs(Fragment, { children: [jsx(Spinner, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.connecting')] })) : (jsxs(Fragment, { children: [jsx(Link2, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.connect')] })) })) : null })), jsxs("div", { className: "flex gap-2", children: [jsx(WithFormPermissions, { name: MCP_SERVER_PERMISSION_NAME, enableRBAC: enableRBAC, permissions: {
1033
- [MCP_SERVER_PERMISSION_NAME]: objectPermissions,
1034
- }, children: ({ disabled }) => !disabled ? (jsxs(Button, { variant: "outline", size: "sm", className: "h-8 flex-1", onClick: () => {
1035
- setEditingServer(server);
1036
- setShowAddConnectorDialog(true);
1037
- }, children: [jsx(SquarePen, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.edit')] })) : null }), jsx(WithFormPermissions, { name: MCP_SERVER_PERMISSION_NAME, enableRBAC: enableRBAC, permissions: {
1038
- [MCP_SERVER_PERMISSION_NAME]: objectPermissions,
1039
- }, children: ({ canDelete }) => canDelete ? (jsxs(Button, { variant: "outline", size: "sm", className: "h-8 flex-1 text-red-600 hover:bg-red-50 hover:text-red-700", onClick: () => setConnectorToDelete({
1040
- id: server.id,
1041
- name: server.name,
1042
- }), disabled: deletingServerId === server.id, children: [jsx(Trash2, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.delete')] })) : null })] })] })) : (
1043
- // Featured servers: Only show Connect/Disconnect for OAuth
1044
- jsx(Fragment, { children: isConnected && connectedServiceId ? (jsx(Button, { variant: "outline", size: "sm", className: "h-8 w-full text-red-600 hover:bg-red-50 hover:text-red-700", onClick: () => handleDisconnectOAuth(server), disabled: isDisconnecting, children: isDisconnecting ? (jsxs(Fragment, { children: [jsx(Spinner, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.disconnecting')] })) : (jsxs(Fragment, { children: [jsx(Unlink, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.disconnect')] })) })) : needsOAuthConnection ? (jsx(Button, { variant: "outline", size: "sm", className: "h-8 w-full border-[#38A1E5] bg-[#38A1E5] text-white hover:bg-[#2d8bc7] hover:text-white", onClick: () => handleConnectOAuth(server), disabled: isConnecting, children: isConnecting ? (jsxs(Fragment, { children: [jsx(Spinner, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.connecting')] })) : (jsxs(Fragment, { children: [jsx(Link2, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.connect')] })) })) : null })) }), onSelect && !needsOAuthConnection && (jsxs(Button, { variant: "default", size: "sm", className: "mt-2 h-8 w-full border-0 bg-gradient-to-r from-[#2563EB] to-[#93C5FD] text-white hover:opacity-90", onClick: async (e) => {
1045
- e.stopPropagation();
1046
- if (!isActive) {
1047
- await handleToggleConnector(server.id, displayName, true);
1048
- }
1049
- onSelect(server);
1050
- }, children: [jsx(Check, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.select')] })), jsx("div", { className: "mt-2 text-xs text-gray-500", children: t('mcpTabConnectorManagementContent.createdDate', {
1051
- date: new Date(server.created_at).toLocaleDateString(),
1052
- }) })] })] }, server.id));
434
+ : t('mcpTabConnectorManagementContent.ariaDisabled', { displayName }), className: "data-[state=checked]:bg-[#38A1E5] data-[state=unchecked]:bg-gray-200" }) })) })] })] }), description && (jsx("p", { className: "truncate text-xs text-gray-500", title: description, children: description })), onSelect && (jsx("div", { className: "mt-auto flex gap-2", children: jsxs(Button, { variant: "default", size: "sm", className: "h-8 border-0 bg-gradient-to-r from-[#2563EB] to-[#93C5FD] text-white hover:opacity-90", onClick: (e) => {
435
+ e.stopPropagation();
436
+ void selectConnector(server);
437
+ }, children: [jsx(Check, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.select')] }) }))] }, server.id));
1053
438
  };
1054
- return (jsxs(Fragment, { children: [jsxs("div", { className: "space-y-6", children: [jsx("div", { className: "rounded-lg border p-4", children: jsxs("div", { className: "flex flex-col gap-3 lg:flex-row", children: [jsx("div", { className: "relative min-w-[200px] flex-1", children: jsx(Input, { type: "text", placeholder: t('mcpTabConnectorManagementContent.searchByName'), value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "w-full bg-transparent" }) }), jsxs(Popover, { children: [jsx(PopoverTrigger, { asChild: true, children: jsxs(Button, { variant: "outline", className: "flex w-full items-center gap-2 bg-transparent font-normal whitespace-nowrap lg:w-auto", children: [jsx(Calendar, { className: "h-4 w-4" }),
1055
- /* istanbul ignore next */ (dateRange === null || dateRange === void 0 ? void 0 : dateRange.from) && (dateRange === null || dateRange === void 0 ? void 0 : dateRange.to)
1056
- ? `${format(dateRange.from, 'MMM dd')} - ${format(dateRange.to, 'MMM dd')}`
1057
- : t('mcpTabConnectorManagementContent.pickDateRange')] }) }), jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: jsx(Calendar$1, { mode: "range", selected: dateRange, onSelect: setDateRange, numberOfMonths: 2, initialFocus: true }) })] }), jsxs(Popover, { children: [jsx(PopoverTrigger, { asChild: true, children: jsxs(Button, { variant: "outline", role: "combobox", "aria-label": t('mcpTabConnectorManagementContent.ariaSelectTransport'), className: "w-full justify-between bg-transparent font-normal lg:w-[200px]", children: [selectedTransport
1058
- ? t(getTransportLabelKey(selectedTransport))
1059
- : t('mcpTabConnectorManagementContent.allTransports'), jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), jsx(PopoverContent, { className: "w-full p-0", align: "start", children: jsxs(Command, { children: [jsx(CommandInput, { placeholder: t('mcpTabConnectorManagementContent.searchTransport') }), jsxs(CommandList, { children: [jsx(CommandEmpty, { children: t('mcpTabConnectorManagementContent.noTransportFound') }), jsx(CommandGroup, { children: TRANSPORT_OPTIONS.map((option) => (jsxs(CommandItem, { value: String(option.value), onSelect: () => setSelectedTransport(option.value), children: [jsx(Check, { className: cn('mr-2 h-4 w-4', selectedTransport === option.value ? 'opacity-100' : 'opacity-0') }), t(getTransportLabelKey(option.value))] }, option.value || 'all'))) })] })] }) })] })] }) }), (featuredOAuthServers.length > 0 || featuredRegularServers.length > 0) && (jsxs("div", { children: [jsx("div", { className: "mb-4 flex items-center justify-between", children: jsx("h3", { className: "text-base font-medium text-gray-900", children: t('mcpTabConnectorManagementContent.featuredConnectors') }) }), isLoadingFeatured ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx(Spinner, { className: "mb-4 h-8 w-8" }), jsx("p", { className: "text-sm text-gray-500", children: t('mcpTabConnectorManagementContent.loadingFeaturedConnectors') })] })) : featuredError ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx("div", { className: "mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100", children: jsx(Plug, { className: "h-8 w-8 text-red-400" }) }), jsx("p", { className: "mb-2 text-sm text-red-600", children: t('mcpTabConnectorManagementContent.failedToLoadFeatured') }), jsx(Button, { variant: "outline", size: "sm", onClick: () => refetchFeatured(), className: "text-sm", children: t('mcpTabConnectorManagementContent.retry') })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(min(100%,15rem),1fr))] gap-4", children: [featuredOAuthServers.map((server) => renderServerCard(server, true)), featuredRegularServers.map((server) => renderServerCard(server, true))] }), featuredTotalPages > 1 && (jsx("div", { className: "flex justify-center pt-4", children: jsx(IblPagination, { currentPage: featuredPage, totalPages: featuredTotalPages, onPageChange: setFeaturedPage, disabled: isLoadingFeatured }) }))] }))] })), jsxs("div", { children: [jsxs("div", { className: "mb-6 flex items-center justify-between", children: [jsx("h3", { className: "text-base font-medium text-gray-900", children: t('mcpTabConnectorManagementContent.connectors') }), jsx(WithPermissions, { rbacResource: "/mcpservers/#create", rbacPermissions: rbacPermissions !== null && rbacPermissions !== void 0 ? rbacPermissions : {}, children: ({ hasPermission }) => hasPermission ? (jsxs(Button, { onClick: () => setShowAddConnectorDialog(true), variant: "outline", className: "ibl-button-primary shrink-0", children: [jsx(Plus, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.addConnector')] })) : null })] }), isLoadingMy ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx(Spinner, { className: "mb-4 h-8 w-8" }), jsx("p", { className: "text-sm text-gray-500", children: t('mcpTabConnectorManagementContent.loadingConnectors') })] })) : myError ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx("div", { className: "mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100", children: jsx(Plug, { className: "h-8 w-8 text-red-400" }) }), jsx("p", { className: "mb-2 text-sm text-red-600", children: t('mcpTabConnectorManagementContent.failedToLoadConnectors') }), jsx(Button, { variant: "outline", size: "sm", onClick: () => refetchMy(), className: "text-sm", children: t('mcpTabConnectorManagementContent.retry') })] })) : !filteredMyServers.length ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx("div", { className: "mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-gray-200", children: jsx(Plug, { className: "h-8 w-8 text-gray-400" }) }), jsx("p", { className: "text-sm text-gray-500", children: t('mcpTabConnectorManagementContent.noConnectorsConfigured') })] })) : (jsxs(Fragment, { children: [jsx("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3", children: filteredMyServers.map((server) => renderServerCard(server, false)) }), myConnectorsTotalPages > 1 && (jsx("div", { className: "flex justify-center pt-4", children: jsx(IblPagination, { currentPage: myConnectorsPage, totalPages: myConnectorsTotalPages, onPageChange: setMyConnectorsPage, disabled: isLoadingMy }) }))] }))] })] }), jsx(ConnectorDialogs, { open: showAddConnectorDialog, onClose: () => {
439
+ const hasFeatured = featuredOAuthServers.length > 0 || featuredRegularServers.length > 0;
440
+ // Decided before the featured page answers: the sub-tabs show from the first
441
+ // render (with the Featured panel's own spinner or error), never swapping the
442
+ // Connectors list away from under the person once it has arrived.
443
+ const showTabs = isLoadingFeatured || !!featuredError || hasFeatured;
444
+ const addButton = (jsx(WithPermissions, { rbacResource: "/mcpservers/#create", rbacPermissions: rbacPermissions !== null && rbacPermissions !== void 0 ? rbacPermissions : {}, children: ({ hasPermission }) => hasPermission ? (jsxs(Button, { onClick: () => setShowAddConnectorDialog(true), variant: "outline", className: "ibl-button-primary shrink-0", children: [jsx(Plus, { className: "mr-2 h-4 w-4" }), t('mcpTabConnectorManagementContent.addConnector')] })) : null }));
445
+ const featuredList = isLoadingFeatured ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx(Spinner, { className: "mb-4 h-8 w-8" }), jsx("p", { className: "text-sm text-gray-500", children: t('mcpTabConnectorManagementContent.loadingFeaturedConnectors') })] })) : featuredError ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx("div", { className: "mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100", children: jsx(Plug, { className: "h-8 w-8 text-red-400" }) }), jsx("p", { className: "mb-2 text-sm text-red-600", children: t('mcpTabConnectorManagementContent.failedToLoadFeatured') }), jsx(Button, { variant: "outline", size: "sm", onClick: () => refetchFeatured(), className: "text-sm", children: t('mcpTabConnectorManagementContent.retry') })] })) : (jsxs(Fragment, { children: [jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [featuredOAuthServers.map((server) => renderServerCard(server, true)), featuredRegularServers.map((server) => renderServerCard(server, true))] }), featuredTotalPages > 1 && (jsx("div", { className: "flex justify-center pt-4", children: jsx(IblPagination, { currentPage: featuredPage, totalPages: featuredTotalPages, onPageChange: setFeaturedPage, disabled: isLoadingFeatured }) }))] }));
446
+ const connectorsList = isLoadingMy ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx(Spinner, { className: "mb-4 h-8 w-8" }), jsx("p", { className: "text-sm text-gray-500", children: t('mcpTabConnectorManagementContent.loadingConnectors') })] })) : myError ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx("div", { className: "mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-100", children: jsx(Plug, { className: "h-8 w-8 text-red-400" }) }), jsx("p", { className: "mb-2 text-sm text-red-600", children: t('mcpTabConnectorManagementContent.failedToLoadConnectors') }), jsx(Button, { variant: "outline", size: "sm", onClick: () => refetchMy(), className: "text-sm", children: t('mcpTabConnectorManagementContent.retry') })] })) : !filteredMyServers.length ? (jsxs("div", { className: "flex flex-col items-center justify-center py-12", children: [jsx("div", { className: "mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-gray-200", children: jsx(Plug, { className: "h-8 w-8 text-gray-400" }) }), jsx("p", { className: "text-sm text-gray-500", children: t('mcpTabConnectorManagementContent.noConnectorsConfigured') })] })) : (jsxs(Fragment, { children: [jsx("div", { className: "grid gap-3 md:grid-cols-2", children: filteredMyServers.map((server) => renderServerCard(server, false)) }), myConnectorsTotalPages > 1 && (jsx("div", { className: "flex justify-center pt-4", children: jsx(IblPagination, { currentPage: myConnectorsPage, totalPages: myConnectorsTotalPages, onPageChange: setMyConnectorsPage, disabled: isLoadingMy }) }))] }));
447
+ return (jsxs(Fragment, { children: [jsxs("div", { className: "space-y-6", children: [jsxs("div", { className: "flex flex-col gap-3 lg:flex-row", children: [jsx("div", { className: "relative min-w-[200px] flex-1", children: jsx(Input, { type: "text", placeholder: t('mcpTabConnectorManagementContent.searchByName'), value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "h-10 w-full bg-transparent" }) }), jsxs(Popover, { children: [jsx(PopoverTrigger, { asChild: true, children: jsxs(Button, { variant: "outline", className: "flex w-full items-center gap-2 bg-transparent font-normal whitespace-nowrap lg:w-auto", children: [jsx(Calendar, { className: "h-4 w-4" }),
448
+ /* istanbul ignore next */ (dateRange === null || dateRange === void 0 ? void 0 : dateRange.from) && (dateRange === null || dateRange === void 0 ? void 0 : dateRange.to)
449
+ ? `${format(dateRange.from, 'MMM dd')} - ${format(dateRange.to, 'MMM dd')}`
450
+ : t('mcpTabConnectorManagementContent.pickDateRange')] }) }), jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: jsx(Calendar$1, { mode: "range", selected: dateRange, onSelect: setDateRange, numberOfMonths: 2, initialFocus: true }) })] }), jsxs(Popover, { children: [jsx(PopoverTrigger, { asChild: true, children: jsxs(Button, { variant: "outline", role: "combobox", "aria-label": t('mcpTabConnectorManagementContent.ariaSelectTransport'), className: "w-full justify-between bg-transparent font-normal lg:w-[200px]", children: [selectedTransport
451
+ ? t(getTransportLabelKey(selectedTransport))
452
+ : t('mcpTabConnectorManagementContent.allTransports'), jsx(ChevronsUpDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })] }) }), jsx(PopoverContent, { className: "w-full p-0", align: "start", children: jsxs(Command, { children: [jsx(CommandInput, { placeholder: t('mcpTabConnectorManagementContent.searchTransport') }), jsxs(CommandList, { children: [jsx(CommandEmpty, { children: t('mcpTabConnectorManagementContent.noTransportFound') }), jsx(CommandGroup, { children: TRANSPORT_OPTIONS.map((option) => (jsxs(CommandItem, { value: String(option.value), onSelect: () => setSelectedTransport(option.value), children: [jsx(Check, { className: cn('mr-2 h-4 w-4', selectedTransport === option.value ? 'opacity-100' : 'opacity-0') }), t(getTransportLabelKey(option.value))] }, option.value || 'all'))) })] })] }) })] })] }), showTabs ? (jsxs(Tabs, { value: section, onValueChange: (value) => setSection(value), className: "space-y-4", children: [jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [jsxs(TabsList, { className: "inline-flex h-auto max-w-full justify-start overflow-x-auto rounded-md bg-gray-100 p-1", "aria-label": t('mcpTabConnectorManagementContent.connectors'), children: [jsx(TabsTrigger, { value: "featured", "data-testid": "mcp-tab-featured", className: SUB_TAB_TRIGGER_CLASS, children: t('mcpTabConnectorManagementContent.featured') }), jsx(TabsTrigger, { value: "custom", "data-testid": "mcp-tab-custom", className: SUB_TAB_TRIGGER_CLASS, children: t('mcpTabConnectorManagementContent.directory') })] }), addButton] }), jsx(TabsContent, { value: "featured", forceMount: true, className: "m-0 data-[state=inactive]:hidden", children: featuredList }), jsx(TabsContent, { value: "custom", forceMount: true, className: "m-0 data-[state=inactive]:hidden", children: connectorsList })] })) : (jsxs("div", { className: "space-y-4", children: [jsxs("div", { className: "flex items-center justify-between", children: [jsx("h3", { className: "text-base font-medium text-gray-900", children: t('mcpTabConnectorManagementContent.connectors') }), addButton] }), connectorsList] }))] }), jsx(ConnectorDialogs, { open: showAddConnectorDialog, restricted: restricted, openOnSignInApp: openOnSignInApp, onClose: () => {
453
+ setRestricted(false);
454
+ setOpenOnSignInApp(false);
1060
455
  setShowAddConnectorDialog(false);
1061
456
  setEditingServer(null);
1062
- }, onAddConnector: handleAddConnectorClick, editingServer: editingServer, editingConnectionId: editingServer ? getConnectionId(editingServer) : null, tenantKey: tenantKey, username: username, mentorId: mentorId, onOAuthComplete: handleOAuthComplete }), connectorToDelete && (jsx(Dialog, { open: true, onOpenChange: () => setConnectorToDelete(null), children: jsxs(DialogContent, { className: "bg-white sm:max-w-[425px]", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), children: [jsx(DialogHeader, { className: "space-y-2", children: jsx(DialogTitle, { className: "text-xl font-semibold text-gray-700", children: t('mcpTabConnectorManagementContent.removeConnector') }) }), jsxs("div", { className: "space-y-4", children: [jsxs("p", { className: "text-sm text-gray-600", children: [t('mcpTabConnectorManagementContent.confirmRemovePrefix'), ' ', jsx("strong", { children: connectorToDelete.name }), t('mcpTabConnectorManagementContent.confirmRemoveSuffix')] }), jsxs("div", { className: "flex justify-end gap-3", children: [jsx(Button, { variant: "outline", onClick: () => setConnectorToDelete(null), disabled: deletingServerId !== null, children: t('mcpTabConnectorManagementContent.cancel') }), jsx(Button, { onClick: () => handleDeleteConnector(connectorToDelete.id, connectorToDelete.name), disabled: deletingServerId !== null, className: "ibl-button-primary", children: deletingServerId === connectorToDelete.id
1063
- ? t('mcpTabConnectorManagementContent.deleting')
1064
- : t('mcpTabConnectorManagementContent.delete') })] })] })] }) }))] }));
457
+ }, onAddConnector: handleAddConnectorClick, onRestrictedSave: handleRestrictedSave, clearAgentScopeOverride: clearAgentScopeOverride, editingServer: editingServer, editingConnectionId: editingServer ? getConnectionId(editingServer) : null, tenantKey: tenantKey, username: username, mentorId: mentorId, onOAuthComplete: handleOAuthComplete, onAuthScopeChanged: handleAuthScopeChanged, connection: editingConnection }), jsx(RemoveConnectorDialog, { connector: connectorToDelete, deletingServerId: deletingServerId, onCancel: () => setConnectorToDelete(null), onConfirm: (connector) => handleDeleteConnector(connector.id, connector.name) })] }));
1065
458
  }
1066
459
 
1067
- export { ConnectorManagementContent, MCP_SERVER_PERMISSION_NAME, TRANSPORT_OPTIONS, checkOAuthConnectionComplete, createMCPServerFormData, createOAuthConnection, determineCheckConnectionAction, determineMessageEventAction, executeCheckConnectionFlowLogic, executeConnectionFoundCleanup, executeHandleMessageCallback, executeMaxPollsCleanup, executeMessageEventFlowLogic, findMCPServerConnection, getTransportLabel, getTransportLabelKey, handleOAuthMessageResult, normalizeTransportValue, processOAuthMessageEvent, processOAuthStorageEvent, shouldExecuteMessageAction, validateDisconnectOAuthParams };
460
+ export { ConnectorManagementContent, MCP_SERVER_PERMISSION_NAME, TRANSPORT_OPTIONS, canManageMcpServers, connectionForScopeExists, connectorImage, currentSignInScope, getMcpConnectionState, getTransportLabelKey, normalizeTransportValue };
1068
461
  //# sourceMappingURL=connector-management-content.js.map