@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.
- package/LICENSE.md +88 -0
- package/README.md +51 -0
- package/biome.jsonc +4 -0
- package/eslint.config.mjs +4 -0
- package/package.json +72 -0
- package/src/SettingsMCPAgentsView.test.ts +256 -0
- package/src/SettingsMCPAgentsView.vue +223 -0
- package/src/SettingsMCPClientsView.test.ts +280 -0
- package/src/SettingsMCPClientsView.vue +168 -0
- package/src/SettingsMCPView.test.ts +832 -0
- package/src/SettingsMCPView.vue +496 -0
- package/src/SettingsMCPWorkflowsView.test.ts +274 -0
- package/src/SettingsMCPWorkflowsView.vue +245 -0
- package/src/__tests__/design-system-icons.test.ts +24 -0
- package/src/__tests__/setup.ts +4 -0
- package/src/assets/client-icons/claude.svg +1 -0
- package/src/assets/client-icons/codex.svg +1 -0
- package/src/assets/client-icons/cursor.svg +1 -0
- package/src/assets/client-icons/gemini.svg +1 -0
- package/src/assets/client-icons/mistral.svg +1 -0
- package/src/assets/client-icons/openai.svg +3 -0
- package/src/assets/client-icons/vscode.svg +1 -0
- package/src/clients.utils.test.ts +85 -0
- package/src/clients.utils.ts +124 -0
- package/src/components/ConnectionParameter.vue +213 -0
- package/src/components/MCPAgentsSelect.vue +176 -0
- package/src/components/MCPEmptyState.vue +75 -0
- package/src/components/MCPWorkflowsSelect.test.ts +384 -0
- package/src/components/MCPWorkflowsSelect.vue +159 -0
- package/src/components/McpAccessToggle.vue +77 -0
- package/src/components/McpAccessTokenTab.test.ts +220 -0
- package/src/components/McpAccessTokenTab.vue +201 -0
- package/src/components/McpAllowedCallbackUrlsDialog.test.ts +122 -0
- package/src/components/McpAllowedCallbackUrlsDialog.vue +359 -0
- package/src/components/McpClientLogoCards.vue +134 -0
- package/src/components/McpConfigSnippet.vue +92 -0
- package/src/components/McpConnectClientDialog.test.ts +115 -0
- package/src/components/McpConnectClientDialog.vue +432 -0
- package/src/components/McpConnectedClientRow.test.ts +86 -0
- package/src/components/McpConnectedClientRow.vue +152 -0
- package/src/components/McpEmptyStateCard.vue +83 -0
- package/src/components/McpStatusControl.test.ts +33 -0
- package/src/components/McpStatusControl.vue +96 -0
- package/src/components/OAuthClientDetailsModal.vue +182 -0
- package/src/components/RevokeOAuthClientConfirmModal.vue +60 -0
- package/src/components/WorkflowLocation.test.ts +329 -0
- package/src/components/WorkflowLocation.vue +198 -0
- package/src/components/tabs/AgentsTable.vue +312 -0
- package/src/components/tabs/OAuthClientOwnerCell.vue +71 -0
- package/src/components/tabs/OAuthClientsFilters.test.ts +86 -0
- package/src/components/tabs/OAuthClientsFilters.vue +201 -0
- package/src/components/tabs/OAuthClientsTable.test.ts +394 -0
- package/src/components/tabs/OAuthClientsTable.vue +473 -0
- package/src/components/tabs/WorkflowsTable.test.ts +661 -0
- package/src/components/tabs/WorkflowsTable.vue +398 -0
- package/src/composables/useMcp.test.ts +59 -0
- package/src/composables/useMcp.ts +61 -0
- package/src/composables/useOAuthClientRevoke.ts +90 -0
- package/src/index.ts +10 -0
- package/src/mcp.api.test.ts +81 -0
- package/src/mcp.api.ts +206 -0
- package/src/mcp.clients.catalog.test.ts +63 -0
- package/src/mcp.clients.catalog.ts +183 -0
- package/src/mcp.constants.ts +45 -0
- package/src/mcp.module.test.ts +106 -0
- package/src/mcp.module.ts +90 -0
- package/src/mcp.store.test.ts +628 -0
- package/src/mcp.store.ts +490 -0
- package/src/mcp.test.utils.ts +57 -0
- package/src/mcp.types.ts +20 -0
- package/src/modals/MCPConnectAgentsModal.test.ts +141 -0
- package/src/modals/MCPConnectAgentsModal.vue +122 -0
- package/src/modals/MCPConnectWorkflowsModal.test.ts +303 -0
- package/src/modals/MCPConnectWorkflowsModal.vue +101 -0
- package/src/redirect-uris.utils.ts +22 -0
- package/stylelint.config.mjs +9 -0
- 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>
|