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