@n8n/frontend-module-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE.md +88 -0
  2. package/README.md +51 -0
  3. package/biome.jsonc +4 -0
  4. package/eslint.config.mjs +4 -0
  5. package/package.json +72 -0
  6. package/src/SettingsMCPAgentsView.test.ts +256 -0
  7. package/src/SettingsMCPAgentsView.vue +223 -0
  8. package/src/SettingsMCPClientsView.test.ts +280 -0
  9. package/src/SettingsMCPClientsView.vue +168 -0
  10. package/src/SettingsMCPView.test.ts +832 -0
  11. package/src/SettingsMCPView.vue +496 -0
  12. package/src/SettingsMCPWorkflowsView.test.ts +274 -0
  13. package/src/SettingsMCPWorkflowsView.vue +245 -0
  14. package/src/__tests__/design-system-icons.test.ts +24 -0
  15. package/src/__tests__/setup.ts +4 -0
  16. package/src/assets/client-icons/claude.svg +1 -0
  17. package/src/assets/client-icons/codex.svg +1 -0
  18. package/src/assets/client-icons/cursor.svg +1 -0
  19. package/src/assets/client-icons/gemini.svg +1 -0
  20. package/src/assets/client-icons/mistral.svg +1 -0
  21. package/src/assets/client-icons/openai.svg +3 -0
  22. package/src/assets/client-icons/vscode.svg +1 -0
  23. package/src/clients.utils.test.ts +85 -0
  24. package/src/clients.utils.ts +124 -0
  25. package/src/components/ConnectionParameter.vue +213 -0
  26. package/src/components/MCPAgentsSelect.vue +176 -0
  27. package/src/components/MCPEmptyState.vue +75 -0
  28. package/src/components/MCPWorkflowsSelect.test.ts +384 -0
  29. package/src/components/MCPWorkflowsSelect.vue +159 -0
  30. package/src/components/McpAccessToggle.vue +77 -0
  31. package/src/components/McpAccessTokenTab.test.ts +220 -0
  32. package/src/components/McpAccessTokenTab.vue +201 -0
  33. package/src/components/McpAllowedCallbackUrlsDialog.test.ts +122 -0
  34. package/src/components/McpAllowedCallbackUrlsDialog.vue +359 -0
  35. package/src/components/McpClientLogoCards.vue +134 -0
  36. package/src/components/McpConfigSnippet.vue +92 -0
  37. package/src/components/McpConnectClientDialog.test.ts +115 -0
  38. package/src/components/McpConnectClientDialog.vue +432 -0
  39. package/src/components/McpConnectedClientRow.test.ts +86 -0
  40. package/src/components/McpConnectedClientRow.vue +152 -0
  41. package/src/components/McpEmptyStateCard.vue +83 -0
  42. package/src/components/McpStatusControl.test.ts +33 -0
  43. package/src/components/McpStatusControl.vue +96 -0
  44. package/src/components/OAuthClientDetailsModal.vue +182 -0
  45. package/src/components/RevokeOAuthClientConfirmModal.vue +60 -0
  46. package/src/components/WorkflowLocation.test.ts +329 -0
  47. package/src/components/WorkflowLocation.vue +198 -0
  48. package/src/components/tabs/AgentsTable.vue +312 -0
  49. package/src/components/tabs/OAuthClientOwnerCell.vue +71 -0
  50. package/src/components/tabs/OAuthClientsFilters.test.ts +86 -0
  51. package/src/components/tabs/OAuthClientsFilters.vue +201 -0
  52. package/src/components/tabs/OAuthClientsTable.test.ts +394 -0
  53. package/src/components/tabs/OAuthClientsTable.vue +473 -0
  54. package/src/components/tabs/WorkflowsTable.test.ts +661 -0
  55. package/src/components/tabs/WorkflowsTable.vue +398 -0
  56. package/src/composables/useMcp.test.ts +59 -0
  57. package/src/composables/useMcp.ts +61 -0
  58. package/src/composables/useOAuthClientRevoke.ts +90 -0
  59. package/src/index.ts +10 -0
  60. package/src/mcp.api.test.ts +81 -0
  61. package/src/mcp.api.ts +206 -0
  62. package/src/mcp.clients.catalog.test.ts +63 -0
  63. package/src/mcp.clients.catalog.ts +183 -0
  64. package/src/mcp.constants.ts +45 -0
  65. package/src/mcp.module.test.ts +106 -0
  66. package/src/mcp.module.ts +90 -0
  67. package/src/mcp.store.test.ts +628 -0
  68. package/src/mcp.store.ts +490 -0
  69. package/src/mcp.test.utils.ts +57 -0
  70. package/src/mcp.types.ts +20 -0
  71. package/src/modals/MCPConnectAgentsModal.test.ts +141 -0
  72. package/src/modals/MCPConnectAgentsModal.vue +122 -0
  73. package/src/modals/MCPConnectWorkflowsModal.test.ts +303 -0
  74. package/src/modals/MCPConnectWorkflowsModal.vue +101 -0
  75. package/src/redirect-uris.utils.ts +22 -0
  76. package/stylelint.config.mjs +9 -0
  77. package/vite.config.ts +52 -0
@@ -0,0 +1,496 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
3
+ import { useRouter } from 'vue-router';
4
+ import { useI18n } from '@n8n/i18n';
5
+ import { capabilities, capabilityRegistry } from '@n8n/frontend-module-sdk';
6
+ import { ElSwitch } from 'element-plus';
7
+ import type { OAuthClientResponseDto } from '@n8n/api-types';
8
+ import {
9
+ N8nButton,
10
+ N8nDialog,
11
+ N8nDialogClose,
12
+ N8nDialogFooter,
13
+ N8nNotice,
14
+ N8nSettingsLayout,
15
+ N8nSettingsPageHeader,
16
+ N8nSettingsRow,
17
+ N8nSettingsRowConfigure,
18
+ N8nSettingsRowGroup,
19
+ N8nSettingsSection,
20
+ } from '@n8n/design-system';
21
+
22
+ import { useDocumentTitle } from '@n8n/composables/useDocumentTitle';
23
+ import { useToast } from '@n8n/composables/useToast';
24
+ import MCPEmptyState from './components/MCPEmptyState.vue';
25
+ import McpAllowedCallbackUrlsDialog from './components/McpAllowedCallbackUrlsDialog.vue';
26
+ import McpConnectClientDialog from './components/McpConnectClientDialog.vue';
27
+ import McpConnectedClientRow from './components/McpConnectedClientRow.vue';
28
+ import McpStatusControl from './components/McpStatusControl.vue';
29
+ import OAuthClientDetailsModal from './components/OAuthClientDetailsModal.vue';
30
+ import RevokeOAuthClientConfirmModal from './components/RevokeOAuthClientConfirmModal.vue';
31
+ import { useMcp } from './composables/useMcp';
32
+ import { useOAuthClientRevoke } from './composables/useOAuthClientRevoke';
33
+ import {
34
+ MCP_AGENTS_VIEW,
35
+ MCP_CLIENTS_VIEW,
36
+ MCP_DOCS_PAGE_URL,
37
+ MCP_WORKFLOWS_VIEW,
38
+ UNKNOWN_COUNT_VALUE,
39
+ } from './mcp.constants';
40
+ import { useMCPStore } from './mcp.store';
41
+ import { useSettingsStore } from '@n8n/stores/settings.store';
42
+ import { useRBACStore } from '@n8n/stores/rbac.store';
43
+
44
+ const i18n = useI18n();
45
+ const toast = useToast();
46
+ const documentTitle = useDocumentTitle({
47
+ releaseChannel: useSettingsStore().settings.releaseChannel,
48
+ });
49
+ const mcp = useMcp();
50
+ const router = useRouter();
51
+
52
+ const mcpStore = useMCPStore();
53
+ const settingsStore = useSettingsStore();
54
+ const rbacStore = useRBACStore();
55
+ const exposeAllOffer = capabilityRegistry.tryUse(capabilities.mcpExposeAllOffer);
56
+ const isExposeAllOfferEnabled = computed(() => exposeAllOffer?.isEnabled() ?? false);
57
+
58
+ const agentsEnabled = computed(() => settingsStore.isAgentsEnabled);
59
+
60
+ const mcpStatusLoading = ref(false);
61
+ const showDisableDialog = ref(false);
62
+ const isLoadingClients = ref(true);
63
+
64
+ const canManageMcpInstance = computed(() => rbacStore.hasScope('mcp:manage'));
65
+ const canToggleMCP = computed(() => canManageMcpInstance.value && !mcpStore.mcpManagedByEnv);
66
+
67
+ const exposedWorkflowsCount = ref<number | null>(null);
68
+ const autoExposeSaving = ref(false);
69
+ const showCallbackUrlsDialog = ref(false);
70
+ const savingCallbackUrls = ref(false);
71
+
72
+ const onAutoExposeSwitchUpdate = (value: string | number | boolean) => {
73
+ void onToggleAutoExpose(value === true);
74
+ };
75
+
76
+ const onToggleAutoExpose = async (value: boolean) => {
77
+ autoExposeSaving.value = true;
78
+ try {
79
+ const updated = await mcpStore.setAutoExposeNewWorkflows(value);
80
+ mcp.trackAutoExposeToggled({ enabled: updated, source: 'settings' });
81
+ } catch (error) {
82
+ toast.showError(error, i18n.baseText('settings.mcp.autoExpose.error.title'));
83
+ } finally {
84
+ autoExposeSaving.value = false;
85
+ }
86
+ };
87
+
88
+ const showInstanceCapacityNotice = computed(
89
+ () => canManageMcpInstance.value && mcpStore.instanceClientStats?.atCapacity === true,
90
+ );
91
+
92
+ const instanceCapacityNoticeContent = computed(() => {
93
+ const stats = mcpStore.instanceClientStats;
94
+ if (!stats) return '';
95
+ return i18n.baseText('settings.mcp.instanceCapacity.warning', {
96
+ interpolate: { count: String(stats.count), limit: String(stats.limit) },
97
+ });
98
+ });
99
+
100
+ const workflowsExposedValue = computed(() =>
101
+ exposedWorkflowsCount.value === null
102
+ ? UNKNOWN_COUNT_VALUE
103
+ : i18n.baseText('settings.mcp.workflowsExposed.count', {
104
+ adjustToNumber: exposedWorkflowsCount.value,
105
+ interpolate: { count: String(exposedWorkflowsCount.value) },
106
+ }),
107
+ );
108
+
109
+ const exposedAgentsCount = ref<number | null>(null);
110
+
111
+ const agentsExposedValue = computed(() =>
112
+ exposedAgentsCount.value === null
113
+ ? UNKNOWN_COUNT_VALUE
114
+ : i18n.baseText('settings.mcp.agentsExposed.count', {
115
+ adjustToNumber: exposedAgentsCount.value,
116
+ interpolate: { count: String(exposedAgentsCount.value) },
117
+ }),
118
+ );
119
+
120
+ const callbackUrlsValue = computed(() =>
121
+ mcpStore.allowedRedirectUris.length === 0
122
+ ? i18n.baseText('settings.mcp.callbackUrls.value.all')
123
+ : i18n.baseText('settings.mcp.callbackUrls.value.count', {
124
+ adjustToNumber: mcpStore.allowedRedirectUris.length,
125
+ interpolate: { count: String(mcpStore.allowedRedirectUris.length) },
126
+ }),
127
+ );
128
+
129
+ const fetchExposedWorkflowsCount = async () => {
130
+ try {
131
+ const response = await mcpStore.fetchWorkflowsAvailableForMCP(1, 1);
132
+ exposedWorkflowsCount.value = response.count;
133
+ } catch (error) {
134
+ toast.showError(error, i18n.baseText('workflows.list.error.fetching'));
135
+ }
136
+ };
137
+
138
+ const fetchExposedAgentsCount = async () => {
139
+ if (!agentsEnabled.value) return;
140
+ try {
141
+ const response = await mcpStore.fetchAgentsAvailableForMCP(1, 1);
142
+ exposedAgentsCount.value = response.count;
143
+ } catch (error) {
144
+ toast.showError(error, i18n.baseText('settings.mcp.agents.list.error.fetching'));
145
+ }
146
+ };
147
+
148
+ const onToggleMCPAccess = async (enabled: boolean) => {
149
+ try {
150
+ mcpStatusLoading.value = true;
151
+ const updated = await mcpStore.setMcpAccessEnabled(enabled);
152
+ if (updated) {
153
+ await Promise.all([
154
+ fetchExposedWorkflowsCount(),
155
+ fetchExposedAgentsCount(),
156
+ fetchConnectedClientsPreview(),
157
+ ]);
158
+ }
159
+ mcp.trackUserToggledMcpAccess(enabled);
160
+ if (enabled && updated) {
161
+ // Best-effort expose-all offer for enrolled users; enabling MCP no longer
162
+ // auto-opens the connect dialog (the user connects a client when ready).
163
+ void exposeAllOffer?.offer(async () => {
164
+ await Promise.all([fetchExposedWorkflowsCount(), fetchExposedAgentsCount()]);
165
+ });
166
+ }
167
+ } catch (error) {
168
+ toast.showError(error, i18n.baseText('settings.mcp.toggle.error'));
169
+ } finally {
170
+ mcpStatusLoading.value = false;
171
+ }
172
+ };
173
+
174
+ const onConfirmDisable = async () => {
175
+ showDisableDialog.value = false;
176
+ await onToggleMCPAccess(false);
177
+ };
178
+
179
+ /**
180
+ * Loads the user's own first clients for the inline preview, along with the
181
+ * totals the "N clients have access" count renders. Only the user's own clients
182
+ * are previewed here; other users' clients stay behind the clients page, which
183
+ * gates them by permission.
184
+ */
185
+ const fetchConnectedClientsPreview = async () => {
186
+ isLoadingClients.value = true;
187
+ try {
188
+ await mcpStore.fetchOAuthClientsPreview();
189
+ isLoadingClients.value = false;
190
+ } catch (error) {
191
+ toast.showError(error, i18n.baseText('settings.mcp.error.fetching.oAuthClients'));
192
+ }
193
+ };
194
+
195
+ const previewClients = computed(() => mcpStore.oauthClientsPreview);
196
+
197
+ /** Instance-wide count when the user can see it, own count otherwise. */
198
+ const connectedClientsTotal = computed(
199
+ () => mcpStore.oauthClientTotals.all ?? mcpStore.oauthClientTotals.mine,
200
+ );
201
+
202
+ // The "View all" row is the section's only content while clients load or when the
203
+ // user has none. Next to a preview it only earns its place when more clients exist
204
+ // than the preview shows (more of the user's own, or other users' for managers).
205
+ const showViewAllRow = computed(
206
+ () =>
207
+ previewClients.value.length === 0 || connectedClientsTotal.value > previewClients.value.length,
208
+ );
209
+
210
+ const onConnectClient = () => {
211
+ mcp.trackConnectClientClicked('settings');
212
+ mcpStore.openConnectPopover();
213
+ };
214
+
215
+ const openClientsView = () => {
216
+ // The preview is the user's own clients, so "View all" continues into that list.
217
+ // With none of their own to land on, a manager goes straight to everyone's.
218
+ const { mine, all } = mcpStore.oauthClientTotals;
219
+ const tab = mine === 0 && (all ?? 0) > 0 ? 'all' : 'mine';
220
+ void router.push({ name: MCP_CLIENTS_VIEW, query: { tab } });
221
+ };
222
+
223
+ const detailsClient = ref<OAuthClientResponseDto | null>(null);
224
+ const detailsOpen = ref(false);
225
+
226
+ const openClientDetails = (client: OAuthClientResponseDto) => {
227
+ detailsClient.value = client;
228
+ detailsOpen.value = true;
229
+ };
230
+
231
+ const { revokeClient, revoking, isRevokingForOther, requestRevoke, cancelRevoke, confirmRevoke } =
232
+ useOAuthClientRevoke({ refreshList: false, onRevoked: fetchConnectedClientsPreview });
233
+
234
+ const openWorkflowsView = () => {
235
+ void router.push({ name: MCP_WORKFLOWS_VIEW });
236
+ };
237
+
238
+ const openAgentsView = () => {
239
+ void router.push({ name: MCP_AGENTS_VIEW });
240
+ };
241
+
242
+ const loadRedirectUris = async () => {
243
+ try {
244
+ await mcpStore.fetchAllowedRedirectUris();
245
+ } catch (error) {
246
+ toast.showError(error, i18n.baseText('settings.mcp.allowedRedirectUris.error.loading'));
247
+ }
248
+ };
249
+
250
+ const onSaveCallbackUrls = async (uris: string[]) => {
251
+ try {
252
+ savingCallbackUrls.value = true;
253
+ await mcpStore.setAllowedRedirectUris(uris);
254
+ showCallbackUrlsDialog.value = false;
255
+ toast.showMessage({
256
+ type: 'success',
257
+ title: i18n.baseText('settings.mcp.allowedRedirectUris.success'),
258
+ });
259
+ } catch (error) {
260
+ toast.showError(error, i18n.baseText('settings.mcp.allowedRedirectUris.error.saving'));
261
+ } finally {
262
+ savingCallbackUrls.value = false;
263
+ }
264
+ };
265
+
266
+ onMounted(async () => {
267
+ documentTitle.set(i18n.baseText('settings.mcp'));
268
+ if (!mcpStore.mcpAccessEnabled) {
269
+ return;
270
+ }
271
+ const fetches: Array<Promise<unknown>> = [
272
+ fetchExposedWorkflowsCount(),
273
+ fetchExposedAgentsCount(),
274
+ fetchConnectedClientsPreview(),
275
+ ];
276
+ if (canManageMcpInstance.value) {
277
+ fetches.push(loadRedirectUris());
278
+ fetches.push(mcpStore.getInstanceClientStats());
279
+ }
280
+ await Promise.all(fetches);
281
+ });
282
+
283
+ // The preview is per-user data; don't let it outlive the page (a soft-redirect
284
+ // logout keeps the store), so the next visit starts from the loading state.
285
+ onBeforeUnmount(() => {
286
+ mcpStore.clearOAuthClientsPreview();
287
+ });
288
+ </script>
289
+
290
+ <template>
291
+ <N8nSettingsLayout :class="$style.layout">
292
+ <N8nSettingsPageHeader
293
+ :title="i18n.baseText('settings.mcp.page.title')"
294
+ :description="i18n.baseText('settings.mcp.page.description')"
295
+ :docs-url="MCP_DOCS_PAGE_URL"
296
+ data-test-id="mcp-settings-header"
297
+ />
298
+
299
+ <MCPEmptyState
300
+ v-if="!mcpStore.mcpAccessEnabled"
301
+ :disabled="!canToggleMCP"
302
+ :loading="mcpStatusLoading"
303
+ :managed-by-env="mcpStore.mcpManagedByEnv"
304
+ @turn-on-mcp="onToggleMCPAccess(true)"
305
+ />
306
+
307
+ <template v-else>
308
+ <N8nNotice
309
+ v-if="showInstanceCapacityNotice"
310
+ theme="warning"
311
+ data-test-id="mcp-instance-capacity-notice"
312
+ :content="instanceCapacityNoticeContent"
313
+ />
314
+
315
+ <N8nSettingsSection
316
+ :title="i18n.baseText('settings.mcp.connectionDetails.title')"
317
+ data-test-id="mcp-enabled-section"
318
+ >
319
+ <N8nSettingsRowGroup>
320
+ <N8nSettingsRow
321
+ :title="i18n.baseText('settings.mcp.status.title')"
322
+ :description="i18n.baseText('settings.mcp.status.description')"
323
+ >
324
+ <template #action>
325
+ <McpStatusControl
326
+ :disabled="!canToggleMCP"
327
+ :loading="mcpStatusLoading"
328
+ :managed-by-env="mcpStore.mcpManagedByEnv"
329
+ @disable="showDisableDialog = true"
330
+ />
331
+ </template>
332
+ </N8nSettingsRow>
333
+ <N8nSettingsRow
334
+ :title="i18n.baseText('settings.mcp.yourClient.title')"
335
+ :description="i18n.baseText('settings.mcp.yourClient.description')"
336
+ >
337
+ <template #action>
338
+ <N8nButton
339
+ variant="outline"
340
+ size="medium"
341
+ icon="mcp"
342
+ :label="i18n.baseText('settings.mcp.yourClient.connect')"
343
+ data-test-id="mcp-connect-client-button"
344
+ @click="onConnectClient"
345
+ />
346
+ </template>
347
+ </N8nSettingsRow>
348
+ </N8nSettingsRowGroup>
349
+ </N8nSettingsSection>
350
+
351
+ <N8nSettingsSection :title="i18n.baseText('settings.mcp.access.title')">
352
+ <N8nSettingsRowGroup>
353
+ <N8nSettingsRow
354
+ :title="i18n.baseText('settings.mcp.workflowsExposed.title')"
355
+ :description="i18n.baseText('settings.mcp.workflowsExposed.description')"
356
+ clickable
357
+ data-test-id="mcp-workflows-exposed-row"
358
+ @click="openWorkflowsView"
359
+ >
360
+ <template #action>
361
+ <N8nSettingsRowConfigure :value="workflowsExposedValue" />
362
+ </template>
363
+ </N8nSettingsRow>
364
+ <N8nSettingsRow
365
+ v-if="canManageMcpInstance && isExposeAllOfferEnabled"
366
+ :title="i18n.baseText('settings.mcp.autoExpose.title')"
367
+ :description="i18n.baseText('settings.mcp.autoExpose.description')"
368
+ >
369
+ <template #action>
370
+ <ElSwitch
371
+ data-test-id="mcp-auto-expose-toggle"
372
+ :model-value="mcpStore.autoExposeNewWorkflows"
373
+ :disabled="mcpStore.mcpManagedByEnv"
374
+ :loading="autoExposeSaving"
375
+ @update:model-value="onAutoExposeSwitchUpdate"
376
+ />
377
+ </template>
378
+ </N8nSettingsRow>
379
+ </N8nSettingsRowGroup>
380
+ <N8nSettingsRowGroup v-if="agentsEnabled">
381
+ <N8nSettingsRow
382
+ :title="i18n.baseText('settings.mcp.agentsExposed.title')"
383
+ :description="i18n.baseText('settings.mcp.agentsExposed.description')"
384
+ clickable
385
+ data-test-id="mcp-agents-exposed-row"
386
+ @click="openAgentsView"
387
+ >
388
+ <template #action>
389
+ <N8nSettingsRowConfigure :value="agentsExposedValue" />
390
+ </template>
391
+ </N8nSettingsRow>
392
+ </N8nSettingsRowGroup>
393
+ <N8nSettingsRowGroup v-if="canManageMcpInstance">
394
+ <N8nSettingsRow
395
+ :title="i18n.baseText('settings.mcp.callbackUrls.title')"
396
+ :description="i18n.baseText('settings.mcp.callbackUrls.description')"
397
+ clickable
398
+ data-test-id="mcp-callback-urls-row"
399
+ @click="showCallbackUrlsDialog = true"
400
+ >
401
+ <template #action>
402
+ <N8nSettingsRowConfigure :value="callbackUrlsValue" />
403
+ </template>
404
+ </N8nSettingsRow>
405
+ </N8nSettingsRowGroup>
406
+ </N8nSettingsSection>
407
+
408
+ <N8nSettingsSection :title="i18n.baseText('settings.mcp.connectedClients.title')">
409
+ <N8nSettingsRowGroup v-if="previewClients.length > 0" data-test-id="mcp-clients-preview">
410
+ <McpConnectedClientRow
411
+ v-for="client in previewClients"
412
+ :key="client.id"
413
+ :client="client"
414
+ :scope-tools="mcpStore.oauthClientScopeTools"
415
+ @click="openClientDetails(client)"
416
+ @revoke="requestRevoke(client)"
417
+ />
418
+ </N8nSettingsRowGroup>
419
+ <N8nSettingsRowGroup v-if="showViewAllRow">
420
+ <N8nSettingsRow
421
+ :title="i18n.baseText('settings.mcp.connectedClients.viewAll.title')"
422
+ :description="
423
+ isLoadingClients
424
+ ? UNKNOWN_COUNT_VALUE
425
+ : i18n.baseText('settings.mcp.connectedClients.viewAll.description', {
426
+ adjustToNumber: connectedClientsTotal,
427
+ interpolate: { count: String(connectedClientsTotal) },
428
+ })
429
+ "
430
+ clickable
431
+ data-test-id="mcp-clients-view-all-row"
432
+ @click="openClientsView"
433
+ >
434
+ <template #action>
435
+ <N8nSettingsRowConfigure
436
+ :value="i18n.baseText('settings.mcp.connectedClients.viewAll.action')"
437
+ />
438
+ </template>
439
+ </N8nSettingsRow>
440
+ </N8nSettingsRowGroup>
441
+ </N8nSettingsSection>
442
+ </template>
443
+
444
+ <N8nDialog
445
+ v-model:open="showDisableDialog"
446
+ size="small"
447
+ :header="i18n.baseText('settings.mcp.status.disableDialog.title')"
448
+ :description="i18n.baseText('settings.mcp.status.disableDialog.description')"
449
+ data-test-id="mcp-disable-dialog"
450
+ >
451
+ <N8nDialogFooter>
452
+ <N8nDialogClose as-child>
453
+ <N8nButton variant="outline" :label="i18n.baseText('generic.cancel')" />
454
+ </N8nDialogClose>
455
+ <N8nButton
456
+ variant="destructive"
457
+ :label="i18n.baseText('settings.mcp.status.disableDialog.confirm')"
458
+ data-test-id="mcp-disable-dialog-confirm"
459
+ @click="onConfirmDisable"
460
+ />
461
+ </N8nDialogFooter>
462
+ </N8nDialog>
463
+
464
+ <McpConnectClientDialog />
465
+
466
+ <OAuthClientDetailsModal
467
+ v-model:open="detailsOpen"
468
+ :client="detailsClient"
469
+ @revoke="requestRevoke"
470
+ />
471
+
472
+ <RevokeOAuthClientConfirmModal
473
+ :client="revokeClient"
474
+ :open="!!revokeClient"
475
+ :loading="revoking"
476
+ :revoking-for-other="!!revokeClient && isRevokingForOther(revokeClient)"
477
+ @confirm="confirmRevoke"
478
+ @cancel="cancelRevoke"
479
+ @update:open="cancelRevoke"
480
+ />
481
+
482
+ <McpAllowedCallbackUrlsDialog
483
+ v-model:open="showCallbackUrlsDialog"
484
+ :uris="mcpStore.allowedRedirectUris"
485
+ :saving="savingCallbackUrls"
486
+ @save="onSaveCallbackUrls"
487
+ />
488
+ </N8nSettingsLayout>
489
+ </template>
490
+
491
+ <style lang="scss" module>
492
+ /* Collapse the layout's own top inset; the settings shell already pads the page top. */
493
+ .layout {
494
+ padding-top: 0;
495
+ }
496
+ </style>