@n8n/frontend-module-mcp 0.3.4 → 1.0.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/package.json +16 -16
- package/src/SettingsMCPView.vue +7 -1
- package/src/components/McpAllowedCallbackUrlsDialog.vue +132 -94
- package/src/components/McpConnectClientDialog.vue +47 -25
- package/src/components/OAuthClientDetailsModal.vue +42 -40
- package/src/mcp.module.ts +2 -1
- package/src/modals/MCPConnectAgentsModal.vue +26 -22
- package/src/modals/MCPConnectWorkflowsModal.vue +12 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@n8n/frontend-module-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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": "
|
|
12
|
-
"@n8n/composables": "
|
|
13
|
-
"@n8n/design-system": "
|
|
14
|
-
"@n8n/frontend-constants": "0.
|
|
15
|
-
"@n8n/frontend-module-sdk": "0.
|
|
16
|
-
"@n8n/i18n": "
|
|
17
|
-
"@n8n/permissions": "0.
|
|
18
|
-
"@n8n/rest-api-client": "
|
|
19
|
-
"@n8n/stores": "
|
|
20
|
-
"@n8n/telemetry": "
|
|
21
|
-
"@n8n/utils": "
|
|
11
|
+
"@n8n/api-types": "2.0.0",
|
|
12
|
+
"@n8n/composables": "2.0.0",
|
|
13
|
+
"@n8n/design-system": "3.0.0",
|
|
14
|
+
"@n8n/frontend-constants": "1.0.0",
|
|
15
|
+
"@n8n/frontend-module-sdk": "1.0.0",
|
|
16
|
+
"@n8n/i18n": "3.0.0",
|
|
17
|
+
"@n8n/permissions": "1.0.0",
|
|
18
|
+
"@n8n/rest-api-client": "3.0.0",
|
|
19
|
+
"@n8n/stores": "3.0.0",
|
|
20
|
+
"@n8n/telemetry": "2.0.0",
|
|
21
|
+
"@n8n/utils": "2.0.0",
|
|
22
22
|
"element-plus": "2.4.3",
|
|
23
23
|
"lodash": "4.18.1",
|
|
24
|
-
"n8n-workflow": "
|
|
24
|
+
"n8n-workflow": "3.0.0",
|
|
25
25
|
"pinia": "^2.2.4",
|
|
26
26
|
"reka-ui": "^2.5.0",
|
|
27
27
|
"vue": "^3.5.13",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"@n8n/frontend-vite-config": "0.1.0",
|
|
36
36
|
"@n8n/playwright-janitor": "0.1.0",
|
|
37
37
|
"@n8n/stylelint-config": "0.0.1",
|
|
38
|
-
"@n8n/typescript-config": "
|
|
39
|
-
"@n8n/vitest-config": "
|
|
38
|
+
"@n8n/typescript-config": "2.0.0",
|
|
39
|
+
"@n8n/vitest-config": "2.0.0",
|
|
40
40
|
"@pinia/testing": "^0.1.6",
|
|
41
41
|
"@testing-library/jest-dom": "^6.6.3",
|
|
42
42
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -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.
|
|
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",
|
package/src/SettingsMCPView.vue
CHANGED
|
@@ -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
|
-
<
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
</
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
</
|
|
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
|
-
</
|
|
220
|
-
</
|
|
221
|
-
</
|
|
222
|
-
</
|
|
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
|
-
<
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
391
|
+
<div v-else data-test-id="mcp-connect-token-setup">
|
|
381
392
|
<McpAccessTokenTab :server-url="serverUrl" @copied="handleTokenTabCopy" />
|
|
382
393
|
</div>
|
|
383
|
-
</
|
|
394
|
+
</N8nDialogBody>
|
|
384
395
|
</N8nDialog>
|
|
385
396
|
</template>
|
|
386
397
|
|
|
387
398
|
<style lang="scss" module>
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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-
|
|
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
|
|
66
|
-
|
|
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
|
-
<
|
|
81
|
-
<
|
|
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.
|
|
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-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
</
|
|
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
|
-
</
|
|
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
|
@@ -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 {
|
|
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
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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"
|