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