@n8n/frontend-module-mcp 0.3.5 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n8n/frontend-module-mcp",
3
- "version": "0.3.5",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -8,20 +8,20 @@
8
8
  "./mcp.module": "./src/mcp.module.ts"
9
9
  },
10
10
  "dependencies": {
11
- "@n8n/api-types": "1.45.4",
12
- "@n8n/composables": "1.35.4",
13
- "@n8n/design-system": "2.44.5",
14
- "@n8n/frontend-constants": "0.11.0",
15
- "@n8n/frontend-module-sdk": "0.15.5",
16
- "@n8n/i18n": "2.45.4",
17
- "@n8n/permissions": "0.82.0",
18
- "@n8n/rest-api-client": "2.45.4",
19
- "@n8n/stores": "2.45.4",
20
- "@n8n/telemetry": "1.13.4",
21
- "@n8n/utils": "1.52.1",
11
+ "@n8n/api-types": "1.46.1",
12
+ "@n8n/composables": "1.36.1",
13
+ "@n8n/design-system": "2.45.1",
14
+ "@n8n/frontend-constants": "0.12.0",
15
+ "@n8n/frontend-module-sdk": "0.16.1",
16
+ "@n8n/i18n": "2.46.1",
17
+ "@n8n/permissions": "0.83.0",
18
+ "@n8n/rest-api-client": "2.46.1",
19
+ "@n8n/stores": "2.46.1",
20
+ "@n8n/telemetry": "1.14.1",
21
+ "@n8n/utils": "1.53.1",
22
22
  "element-plus": "2.4.3",
23
23
  "lodash": "4.18.1",
24
- "n8n-workflow": "2.45.2",
24
+ "n8n-workflow": "2.46.1",
25
25
  "pinia": "^2.2.4",
26
26
  "reka-ui": "^2.5.0",
27
27
  "vue": "^3.5.13",
@@ -49,7 +49,7 @@
49
49
  "stylelint": "^16.23.0",
50
50
  "typescript": "6.0.2",
51
51
  "unplugin-icons": "^23.0.1",
52
- "vite": "^8.0.2",
52
+ "vite": "^8.1.6",
53
53
  "vite-svg-loader": "5.1.0",
54
54
  "vitest": "5.0.1",
55
55
  "vitest-mock-extended": "^3.1.0",
@@ -8,7 +8,9 @@ import type { OAuthClientResponseDto } from '@n8n/api-types';
8
8
  import {
9
9
  N8nButton,
10
10
  N8nDialog,
11
+ N8nDialogBody,
11
12
  N8nDialogClose,
13
+ N8nDialogDescription,
12
14
  N8nDialogFooter,
13
15
  N8nNotice,
14
16
  N8nSettingsLayout,
@@ -466,9 +468,13 @@ onBeforeUnmount(() => {
466
468
  v-model:open="showDisableDialog"
467
469
  size="small"
468
470
  :header="i18n.baseText('settings.mcp.status.disableDialog.title')"
469
- :description="i18n.baseText('settings.mcp.status.disableDialog.description')"
470
471
  data-test-id="mcp-disable-dialog"
471
472
  >
473
+ <N8nDialogBody>
474
+ <N8nDialogDescription>
475
+ {{ i18n.baseText('settings.mcp.status.disableDialog.description') }}
476
+ </N8nDialogDescription>
477
+ </N8nDialogBody>
472
478
  <N8nDialogFooter>
473
479
  <N8nDialogClose as-child>
474
480
  <N8nButton variant="outline" :label="i18n.baseText('generic.cancel')" />
@@ -6,6 +6,8 @@ import {
6
6
  N8nAnimatedCollapsibleContent,
7
7
  N8nButton,
8
8
  N8nDialog,
9
+ N8nDialogBody,
10
+ N8nDialogDescription,
9
11
  N8nDialogFooter,
10
12
  N8nInput,
11
13
  N8nRadioGroup,
@@ -92,10 +94,40 @@ const canSave = computed(() => {
92
94
  return true;
93
95
  });
94
96
 
97
+ const addUrlRow = ref<HTMLElement | null>(null);
98
+
95
99
  const addUrl = () => {
96
100
  drafts.value.push(createDraft());
97
101
  };
98
102
 
103
+ /** The dialog body that actually scrolls, if this node sits inside one. */
104
+ function scrollParent(node: HTMLElement): HTMLElement | null {
105
+ let current = node.parentElement;
106
+ while (current) {
107
+ const { overflowY } = getComputedStyle(current);
108
+ if (overflowY === 'auto' || overflowY === 'scroll') return current;
109
+ current = current.parentElement;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ // The new row grows in after the click. Once that motion ends, bring the add
115
+ // button back if the list now runs past the dialog body. Reduced-motion users
116
+ // get an instant jump; the row transitions already drop their motion too.
117
+ function keepAddButtonInView() {
118
+ const row = addUrlRow.value;
119
+ if (!row) return;
120
+ const scroller = scrollParent(row);
121
+ if (!scroller || scroller.scrollHeight <= scroller.clientHeight) return;
122
+ const rowRect = row.getBoundingClientRect();
123
+ const scrollerRect = scroller.getBoundingClientRect();
124
+ if (rowRect.bottom <= scrollerRect.bottom) return;
125
+ const behavior = window.matchMedia('(prefers-reduced-motion: reduce)').matches
126
+ ? 'auto'
127
+ : 'smooth';
128
+ scroller.scrollTo({ top: scroller.scrollHeight - scroller.clientHeight, behavior });
129
+ }
130
+
99
131
  const removeUrl = (id: number) => {
100
132
  // The list never goes empty. Clearing the last row keeps its identity, so the
101
133
  // input just empties instead of leaving and re-entering.
@@ -115,111 +147,118 @@ const onSave = () => emit('save', result.value);
115
147
  :open="open"
116
148
  size="medium"
117
149
  :header="i18n.baseText('settings.mcp.callbackUrls.dialog.title')"
118
- :description="i18n.baseText('settings.mcp.callbackUrls.dialog.description')"
119
150
  data-test-id="mcp-callback-urls-dialog"
120
151
  @update:open="emit('update:open', $event)"
121
152
  >
122
- <div :class="$style.body">
123
- <N8nRadioGroup v-model="mode" orientation="vertical" :class="$style.modes">
124
- <div
125
- v-for="option in ['all', 'trusted'] as const"
126
- :key="option"
127
- :class="[$style['mode-card'], mode === option && $style['mode-card-active']]"
128
- :data-test-id="`mcp-callback-urls-mode-${option}`"
129
- @click="mode = option"
130
- >
131
- <N8nRadioGroupItem
132
- :value="option"
133
- :label="i18n.baseText(`settings.mcp.callbackUrls.mode.${option}.label`)"
134
- :description="i18n.baseText(`settings.mcp.callbackUrls.mode.${option}.description`)"
135
- />
136
- </div>
137
- </N8nRadioGroup>
138
-
139
- <!-- The URL list unfolds with the settings-surface blur motion (the same
153
+ <N8nDialogBody>
154
+ <N8nDialogDescription>
155
+ {{ i18n.baseText('settings.mcp.callbackUrls.dialog.description') }}
156
+ </N8nDialogDescription>
157
+ <div :class="$style.body">
158
+ <N8nRadioGroup v-model="mode" orientation="vertical" :class="$style.modes">
159
+ <div
160
+ v-for="option in ['all', 'trusted'] as const"
161
+ :key="option"
162
+ :class="[$style['mode-card'], mode === option && $style['mode-card-active']]"
163
+ :data-test-id="`mcp-callback-urls-mode-${option}`"
164
+ @click="mode = option"
165
+ >
166
+ <N8nRadioGroupItem
167
+ :value="option"
168
+ :label="i18n.baseText(`settings.mcp.callbackUrls.mode.${option}.label`)"
169
+ :description="i18n.baseText(`settings.mcp.callbackUrls.mode.${option}.description`)"
170
+ />
171
+ </div>
172
+ </N8nRadioGroup>
173
+
174
+ <!-- The URL list unfolds with the settings-surface blur motion (the same
140
175
  N8nSettingsRow's expand region uses) so the dialog grows and shrinks
141
176
  with it instead of jumping between the two heights. -->
142
- <CollapsibleRoot :open="mode === 'trusted'">
143
- <N8nAnimatedCollapsibleContent
144
- :class="$style['trusted-content']"
145
- blur
146
- data-test-id="mcp-callback-urls-trusted"
147
- >
148
- <div :class="$style.trusted">
149
- <N8nText size="small" color="text-dark" bold :class="$style['trusted-label']">
150
- {{ i18n.baseText('settings.mcp.callbackUrls.trusted.label') }}
151
- </N8nText>
152
- <!-- Rows grow in and shrink out with the same motion as the section, so
177
+ <CollapsibleRoot :open="mode === 'trusted'">
178
+ <N8nAnimatedCollapsibleContent
179
+ :class="$style['trusted-content']"
180
+ blur
181
+ data-test-id="mcp-callback-urls-trusted"
182
+ >
183
+ <div :class="$style.trusted">
184
+ <N8nText size="small" color="text-dark" bold :class="$style['trusted-label']">
185
+ {{ i18n.baseText('settings.mcp.callbackUrls.trusted.label') }}
186
+ </N8nText>
187
+ <!-- Rows grow in and shrink out with the same motion as the section, so
153
188
  adding or removing a URL never snaps the dialog to a new height. -->
154
- <TransitionGroup
155
- tag="div"
156
- :class="$style['url-list']"
157
- :enter-from-class="$style['slot-collapsed']"
158
- :enter-active-class="$style['slot-animating']"
159
- :leave-active-class="$style['slot-animating']"
160
- :leave-to-class="$style['slot-collapsed']"
161
- >
162
- <div
163
- v-for="draft in drafts"
164
- :key="draft.id"
165
- :class="$style.slot"
166
- data-test-id="mcp-callback-url-row"
189
+ <TransitionGroup
190
+ tag="div"
191
+ :class="$style['url-list']"
192
+ :enter-from-class="$style['slot-collapsed']"
193
+ :enter-active-class="$style['slot-animating']"
194
+ :leave-active-class="$style['slot-animating']"
195
+ :leave-to-class="$style['slot-collapsed']"
196
+ @after-enter="keepAddButtonInView"
167
197
  >
168
- <div :class="$style['slot-clip']">
169
- <div :class="$style['url-row']">
170
- <N8nInput
171
- v-model="draft.value"
172
- type="text"
173
- :placeholder="i18n.baseText('settings.mcp.callbackUrls.trusted.placeholder')"
174
- data-test-id="mcp-callback-url-input"
175
- />
176
- <N8nButton
177
- v-if="drafts.length > 1 || draft.value.length > 0"
178
- variant="ghost"
179
- size="small"
180
- icon-only
181
- icon="x"
182
- :aria-label="i18n.baseText('generic.delete')"
183
- data-test-id="mcp-callback-url-remove"
184
- @click="removeUrl(draft.id)"
185
- />
198
+ <div
199
+ v-for="draft in drafts"
200
+ :key="draft.id"
201
+ :class="$style.slot"
202
+ data-test-id="mcp-callback-url-row"
203
+ >
204
+ <div :class="$style['slot-clip']">
205
+ <div :class="$style['url-row']">
206
+ <N8nInput
207
+ v-model="draft.value"
208
+ type="text"
209
+ :placeholder="
210
+ i18n.baseText('settings.mcp.callbackUrls.trusted.placeholder')
211
+ "
212
+ data-test-id="mcp-callback-url-input"
213
+ />
214
+ <N8nButton
215
+ v-if="drafts.length > 1 || draft.value.length > 0"
216
+ variant="ghost"
217
+ size="small"
218
+ icon-only
219
+ icon="x"
220
+ :aria-label="i18n.baseText('generic.delete')"
221
+ data-test-id="mcp-callback-url-remove"
222
+ @click="removeUrl(draft.id)"
223
+ />
224
+ </div>
186
225
  </div>
187
226
  </div>
188
- </div>
189
- </TransitionGroup>
190
- <Transition
191
- :enter-from-class="$style['slot-collapsed']"
192
- :enter-active-class="$style['slot-animating']"
193
- :leave-active-class="$style['slot-animating']"
194
- :leave-to-class="$style['slot-collapsed']"
195
- >
196
- <div v-if="validationError" :class="$style.slot">
197
- <div :class="$style['slot-clip']">
198
- <N8nText
199
- size="small"
200
- color="danger"
201
- :class="$style.error"
202
- data-test-id="mcp-callback-urls-error"
203
- >
204
- {{ validationError }}
205
- </N8nText>
227
+ </TransitionGroup>
228
+ <Transition
229
+ :enter-from-class="$style['slot-collapsed']"
230
+ :enter-active-class="$style['slot-animating']"
231
+ :leave-active-class="$style['slot-animating']"
232
+ :leave-to-class="$style['slot-collapsed']"
233
+ >
234
+ <div v-if="validationError" :class="$style.slot">
235
+ <div :class="$style['slot-clip']">
236
+ <N8nText
237
+ size="small"
238
+ color="danger"
239
+ :class="$style.error"
240
+ data-test-id="mcp-callback-urls-error"
241
+ >
242
+ {{ validationError }}
243
+ </N8nText>
244
+ </div>
206
245
  </div>
246
+ </Transition>
247
+ <div ref="addUrlRow">
248
+ <N8nButton
249
+ variant="outline"
250
+ size="small"
251
+ icon="plus"
252
+ :label="i18n.baseText('settings.mcp.callbackUrls.trusted.addUrl')"
253
+ data-test-id="mcp-callback-url-add"
254
+ @click="addUrl"
255
+ />
207
256
  </div>
208
- </Transition>
209
- <div>
210
- <N8nButton
211
- variant="outline"
212
- size="small"
213
- icon="plus"
214
- :label="i18n.baseText('settings.mcp.callbackUrls.trusted.addUrl')"
215
- data-test-id="mcp-callback-url-add"
216
- @click="addUrl"
217
- />
218
257
  </div>
219
- </div>
220
- </N8nAnimatedCollapsibleContent>
221
- </CollapsibleRoot>
222
- </div>
258
+ </N8nAnimatedCollapsibleContent>
259
+ </CollapsibleRoot>
260
+ </div>
261
+ </N8nDialogBody>
223
262
 
224
263
  <N8nDialogFooter>
225
264
  <N8nButton variant="outline" :label="i18n.baseText('generic.cancel')" @click="onCancel" />
@@ -249,7 +288,6 @@ $slot-easing: motion.$blur-motion-easing;
249
288
  .body {
250
289
  display: flex;
251
290
  flex-direction: column;
252
- margin-block: var(--spacing--xs);
253
291
  }
254
292
 
255
293
  .modes {
@@ -5,6 +5,10 @@ import type { BaseTextKey } from '@n8n/i18n';
5
5
  import {
6
6
  N8nButton,
7
7
  N8nDialog,
8
+ N8nDialogBody,
9
+ N8nDialogDescription,
10
+ N8nDialogHeader,
11
+ N8nDialogTitle,
8
12
  N8nDropdownMenu,
9
13
  N8nIcon,
10
14
  N8nSettingsRow,
@@ -155,20 +159,27 @@ const serverUrlDescription = computed(() =>
155
159
  <N8nDialog
156
160
  :open="mcpStore.connectPopoverOpen"
157
161
  size="large"
158
- :header="i18n.baseText('settings.mcp.connectDialog.title')"
159
- :description="i18n.baseText('settings.mcp.connectDialog.description')"
160
162
  data-test-id="mcp-connect-dialog"
161
163
  @update:open="onOpenChange"
162
164
  >
163
- <div :class="$style.body">
164
- <N8nTabs
165
- :model-value="activeMethod"
166
- :options="methodTabs"
167
- :class="$style.tabs"
168
- data-test-id="mcp-connect-method-tabs"
169
- @update:model-value="activeMethod = $event"
170
- />
171
-
165
+ <N8nDialogHeader :class="$style.header">
166
+ <div :class="$style['header-main']">
167
+ <N8nDialogTitle>
168
+ {{ i18n.baseText('settings.mcp.connectDialog.title') }}
169
+ </N8nDialogTitle>
170
+ <N8nDialogDescription :class="$style.description">
171
+ {{ i18n.baseText('settings.mcp.connectDialog.description') }}
172
+ </N8nDialogDescription>
173
+ <N8nTabs
174
+ :model-value="activeMethod"
175
+ :options="methodTabs"
176
+ :class="$style.tabs"
177
+ data-test-id="mcp-connect-method-tabs"
178
+ @update:model-value="activeMethod = $event"
179
+ />
180
+ </div>
181
+ </N8nDialogHeader>
182
+ <N8nDialogBody :class="$style.body">
172
183
  <N8nSettingsRowGroup v-if="activeMethod === 'oauth'">
173
184
  <N8nSettingsRow
174
185
  :title="i18n.baseText('settings.mcp.connectDialog.yourClient.title')"
@@ -377,25 +388,40 @@ const serverUrlDescription = computed(() =>
377
388
  </template>
378
389
  </N8nSettingsRowGroup>
379
390
 
380
- <div v-else :class="$style['token-setup']" data-test-id="mcp-connect-token-setup">
391
+ <div v-else data-test-id="mcp-connect-token-setup">
381
392
  <McpAccessTokenTab :server-url="serverUrl" @copied="handleTokenTabCopy" />
382
393
  </div>
383
- </div>
394
+ </N8nDialogBody>
384
395
  </N8nDialog>
385
396
  </template>
386
397
 
387
398
  <style lang="scss" module>
388
- /* Body-only scroll so the dialog header stays pinned and tall clients
389
- (e.g. Claude Code's config block) never run off-screen. */
390
- .body {
391
- max-height: calc(100dvh - 16rem);
392
- /* Extra breathing room so the OAuth/API-key tabs don't crowd the description. */
393
- margin-top: var(--spacing--md);
394
- overflow-y: auto;
399
+ .header {
400
+ border-bottom: var(--border);
401
+ padding-block-end: 0;
402
+ }
403
+
404
+ /* One column so the title, description, and tabs stack. The header row
405
+ otherwise lays its children out beside the close button. */
406
+ .header-main {
407
+ display: flex;
408
+ flex: 1 1 auto;
409
+ flex-direction: column;
410
+ gap: var(--spacing--2xs);
411
+ min-width: 0;
412
+ width: 100%;
413
+ }
414
+
415
+ .description {
416
+ margin: 0;
395
417
  }
396
418
 
397
419
  .tabs {
398
- margin-bottom: var(--spacing--sm);
420
+ margin-top: var(--spacing--2xs);
421
+ }
422
+
423
+ .body {
424
+ padding-block-start: var(--n8n-dialog-region--padding, var(--spacing--md));
399
425
  }
400
426
 
401
427
  .picker-trigger {
@@ -435,8 +461,4 @@ const serverUrlDescription = computed(() =>
435
461
  .copy-field {
436
462
  width: 100%;
437
463
  }
438
-
439
- .token-setup {
440
- margin-top: var(--spacing--2xs);
441
- }
442
464
  </style>
@@ -5,6 +5,7 @@ import type { OAuthClientResponseDto } from '@n8n/api-types';
5
5
  import {
6
6
  N8nButton,
7
7
  N8nDialog,
8
+ N8nDialogBody,
8
9
  N8nDialogDescription,
9
10
  N8nDialogFooter,
10
11
  N8nDialogHeader,
@@ -62,8 +63,13 @@ function onRevoke() {
62
63
  </script>
63
64
 
64
65
  <template>
65
- <N8nDialog :open="open" size="medium" @update:open="emit('update:open', $event)">
66
- <div v-if="client" :class="$style.container" data-test-id="mcp-client-details-modal">
66
+ <N8nDialog
67
+ :open="open"
68
+ size="medium"
69
+ data-test-id="mcp-client-details-modal"
70
+ @update:open="emit('update:open', $event)"
71
+ >
72
+ <template v-if="client">
67
73
  <N8nDialogHeader>
68
74
  <N8nDialogTitle>
69
75
  <span :class="$style.title">
@@ -77,42 +83,44 @@ function onRevoke() {
77
83
  <N8nDialogDescription>{{ subtitle }}</N8nDialogDescription>
78
84
  </N8nDialogHeader>
79
85
 
80
- <div :class="$style.details">
81
- <template v-if="ownerLabel">
86
+ <N8nDialogBody>
87
+ <div :class="$style.details">
88
+ <template v-if="ownerLabel">
89
+ <N8nText color="text-light" size="small">
90
+ {{ i18n.baseText('settings.mcp.oAuthClients.details.connectedBy') }}
91
+ </N8nText>
92
+ <N8nText color="text-dark" size="small" data-test-id="mcp-client-details-connected-by">
93
+ {{ ownerLabel }}
94
+ </N8nText>
95
+ </template>
96
+
82
97
  <N8nText color="text-light" size="small">
83
- {{ i18n.baseText('settings.mcp.oAuthClients.details.connectedBy') }}
98
+ {{ i18n.baseText('settings.mcp.oAuthClients.details.connectedOn') }}
84
99
  </N8nText>
85
- <N8nText color="text-dark" size="small" data-test-id="mcp-client-details-connected-by">
86
- {{ ownerLabel }}
100
+ <N8nText color="text-dark" size="small" data-test-id="mcp-client-details-connected-on">
101
+ <N8nTimeAgo
102
+ :date="new Date(client.grantedAt).toISOString()"
103
+ capitalize
104
+ :locale="rootStore.defaultLocale"
105
+ />
87
106
  </N8nText>
88
- </template>
89
-
90
- <N8nText color="text-light" size="small">
91
- {{ i18n.baseText('settings.mcp.oAuthClients.details.connectedOn') }}
92
- </N8nText>
93
- <N8nText color="text-dark" size="small" data-test-id="mcp-client-details-connected-on">
94
- <N8nTimeAgo
95
- :date="new Date(client.grantedAt).toISOString()"
96
- capitalize
97
- :locale="rootStore.defaultLocale"
98
- />
99
- </N8nText>
100
-
101
- <N8nText color="text-light" size="small">
102
- {{ i18n.baseText('settings.mcp.oAuthClients.details.access') }}
103
- </N8nText>
104
- <div :class="$style.access" data-test-id="mcp-client-details-access">
105
- <N8nText
106
- v-for="scope in grantedScopes"
107
- :key="scope"
108
- color="text-dark"
109
- size="small"
110
- :data-test-id="`mcp-client-details-scope-${scope}`"
111
- >
112
- {{ scopeLabel(i18n, scope) }}
107
+
108
+ <N8nText color="text-light" size="small">
109
+ {{ i18n.baseText('settings.mcp.oAuthClients.details.access') }}
113
110
  </N8nText>
111
+ <div :class="$style.access" data-test-id="mcp-client-details-access">
112
+ <N8nText
113
+ v-for="scope in grantedScopes"
114
+ :key="scope"
115
+ color="text-dark"
116
+ size="small"
117
+ :data-test-id="`mcp-client-details-scope-${scope}`"
118
+ >
119
+ {{ scopeLabel(i18n, scope) }}
120
+ </N8nText>
121
+ </div>
114
122
  </div>
115
- </div>
123
+ </N8nDialogBody>
116
124
 
117
125
  <N8nDialogFooter>
118
126
  <N8nButton
@@ -126,17 +134,11 @@ function onRevoke() {
126
134
  {{ i18n.baseText('settings.mcp.oAuthClients.table.action.revokeAccess') }}
127
135
  </N8nButton>
128
136
  </N8nDialogFooter>
129
- </div>
137
+ </template>
130
138
  </N8nDialog>
131
139
  </template>
132
140
 
133
141
  <style lang="scss" module>
134
- .container {
135
- display: flex;
136
- flex-direction: column;
137
- gap: var(--spacing--sm);
138
- }
139
-
140
142
  .title {
141
143
  display: inline-flex;
142
144
  align-items: center;
package/src/mcp.module.ts CHANGED
@@ -71,7 +71,8 @@ export const MCPModule = defineFrontendModule({
71
71
  settingsPages: [
72
72
  {
73
73
  id: 'settings-mcp',
74
- order: 200,
74
+ order: 10,
75
+ section: 'ai',
75
76
  icon: 'mcp',
76
77
  get label() {
77
78
  return useI18n().baseText('settings.mcp');
@@ -1,7 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import { useI18n } from '@n8n/i18n';
3
3
  import MCPAgentsSelect from '../components/MCPAgentsSelect.vue';
4
- import { N8nButton, N8nDialog, N8nDialogFooter, N8nNotice } from '@n8n/design-system';
4
+ import {
5
+ N8nButton,
6
+ N8nDialog,
7
+ N8nDialogBody,
8
+ N8nDialogFooter,
9
+ N8nNotice,
10
+ } from '@n8n/design-system';
5
11
  import { computed, ref, watch } from 'vue';
6
12
  import { useTelemetry } from '@n8n/composables/useTelemetry';
7
13
  import { TELEMETRY_EVENT } from '@n8n/telemetry';
@@ -59,10 +65,6 @@ function onConfirm() {
59
65
  void save();
60
66
  }
61
67
  }
62
-
63
- function preventOutsideClose(event: Event) {
64
- event.preventDefault();
65
- }
66
68
  </script>
67
69
 
68
70
  <template>
@@ -70,25 +72,27 @@ function preventOutsideClose(event: Event) {
70
72
  v-model:open="open"
71
73
  size="xlarge"
72
74
  :header="i18n.baseText('settings.mcp.connectAgents.modalTitle')"
75
+ :close-on-overlay-click="false"
73
76
  data-test-id="mcp-connect-agents-dialog"
74
- @interact-outside="preventOutsideClose"
75
77
  >
76
- <div :class="$style.content">
77
- <N8nNotice
78
- data-test-id="mcp-connect-agents-info-notice"
79
- theme="info"
80
- :content="i18n.baseText('settings.mcp.connectAgents.notice')"
81
- :class="$style.notice"
82
- />
83
- <MCPAgentsSelect
84
- ref="selectRef"
85
- v-model="selectedAgentIds"
86
- :placeholder="i18n.baseText('settings.mcp.connectAgents.input.placeholder')"
87
- :disabled="isSaving"
88
- @ready="onSelectReady"
89
- @confirm="onConfirm"
90
- />
91
- </div>
78
+ <N8nDialogBody>
79
+ <div :class="$style.content">
80
+ <N8nNotice
81
+ data-test-id="mcp-connect-agents-info-notice"
82
+ theme="info"
83
+ :content="i18n.baseText('settings.mcp.connectAgents.notice')"
84
+ :class="$style.notice"
85
+ />
86
+ <MCPAgentsSelect
87
+ ref="selectRef"
88
+ v-model="selectedAgentIds"
89
+ :placeholder="i18n.baseText('settings.mcp.connectAgents.input.placeholder')"
90
+ :disabled="isSaving"
91
+ @ready="onSelectReady"
92
+ @confirm="onConfirm"
93
+ />
94
+ </div>
95
+ </N8nDialogBody>
92
96
  <N8nDialogFooter>
93
97
  <N8nButton
94
98
  variant="outline"
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { useI18n } from '@n8n/i18n';
3
3
  import MCPWorkflowsSelect from '../components/MCPWorkflowsSelect.vue';
4
- import { N8nButton, N8nDialog, N8nDialogFooter } from '@n8n/design-system';
4
+ import { N8nButton, N8nDialog, N8nDialogBody, N8nDialogFooter } from '@n8n/design-system';
5
5
  import { computed, ref, watch } from 'vue';
6
6
  import { useTelemetry } from '@n8n/composables/useTelemetry';
7
7
 
@@ -58,10 +58,6 @@ function onConfirm() {
58
58
  void save();
59
59
  }
60
60
  }
61
-
62
- function preventOutsideClose(event: Event) {
63
- event.preventDefault();
64
- }
65
61
  </script>
66
62
 
67
63
  <template>
@@ -69,17 +65,19 @@ function preventOutsideClose(event: Event) {
69
65
  v-model:open="open"
70
66
  size="xlarge"
71
67
  :header="i18n.baseText('settings.mcp.connectWorkflows.modalTitle')"
68
+ :close-on-overlay-click="false"
72
69
  data-test-id="mcp-connect-workflows-dialog"
73
- @interact-outside="preventOutsideClose"
74
70
  >
75
- <MCPWorkflowsSelect
76
- ref="selectRef"
77
- v-model="selectedWorkflowIds"
78
- :placeholder="i18n.baseText('settings.mcp.connectWorkflows.input.placeholder')"
79
- :disabled="isSaving"
80
- @ready="onSelectReady"
81
- @confirm="onConfirm"
82
- />
71
+ <N8nDialogBody>
72
+ <MCPWorkflowsSelect
73
+ ref="selectRef"
74
+ v-model="selectedWorkflowIds"
75
+ :placeholder="i18n.baseText('settings.mcp.connectWorkflows.input.placeholder')"
76
+ :disabled="isSaving"
77
+ @ready="onSelectReady"
78
+ @confirm="onConfirm"
79
+ />
80
+ </N8nDialogBody>
83
81
  <N8nDialogFooter>
84
82
  <N8nButton
85
83
  variant="outline"