diff --git a/packages/cli/src/modules/mcp/mcp.controller.ts b/packages/cli/src/modules/mcp/mcp.controller.ts index cb854413189..a6e915a6853 100644 --- a/packages/cli/src/modules/mcp/mcp.controller.ts +++ b/packages/cli/src/modules/mcp/mcp.controller.ts @@ -13,7 +13,7 @@ export type FlushableResponse = Response & { flush: () => void }; const getAuthMiddleware = () => Container.get(McpServerApiKeyService).getAuthMiddleware(); -@RootLevelController('/mcp-access') +@RootLevelController('/mcp-server') export class McpController { constructor( private readonly errorReporter: ErrorReporter, diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index de5f84cfeda..47b76840a59 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -2028,11 +2028,15 @@ "settings.mcp.toggle.error": "Error updating MCP access", "settings.mcp.instructions.enableAccess": "Enable workflow access in at least one workflow via its settings", "settings.mcp.instructions.serverUrl": "Server URL", - "settings.mcp.instructions.apiKey.part1": "Create an", - "settings.mcp.instructions.apiKey.part2": "No scopes required, MCP uses your role for authorization", + "settings.mcp.instructions.apiKey.label": "Access token", "settings.mcp.instructions.json": "Or use the following code in your 'mcp.json' file", "settings.mcp.instructions.docs.part1": "For more detailed instructions and examples, have a look at", "settings.mcp.instructions.docs.part2": "our docs", + "settings.mcp.instructions.rotateKey.tooltip": "Generate new token.
This one will be automatically revoked.", + "settings.mcp.instructions.apiKey.tip": "Use this as an authorization token. May be named differently in different clients", + "settings.mcp.newKey.notice": "Make sure to copy your access token now. You won’t be able to see or copy it again!", + "settings.mcp.error.fetching.apiKey": "Error fetching access token", + "settings.mcp.error.rotating.apiKey": "Error generating new access token", "settings.goBack": "Go back", "settings.personal": "Personal", "settings.personal.basicInformation": "Basic Information", diff --git a/packages/frontend/editor-ui/src/features/mcpAccess/SettingsMCPView.vue b/packages/frontend/editor-ui/src/features/mcpAccess/SettingsMCPView.vue index 80e92864a63..1716bd2382d 100644 --- a/packages/frontend/editor-ui/src/features/mcpAccess/SettingsMCPView.vue +++ b/packages/frontend/editor-ui/src/features/mcpAccess/SettingsMCPView.vue @@ -14,6 +14,7 @@ import { useMCPStore } from '@/features/mcpAccess/mcp.store'; import { useUsersStore } from '@/stores/users.store'; import MCPConnectionInstructions from '@/features/mcpAccess/components/MCPConnectionInstructions.vue'; import ProjectIcon from '@/components/Projects/ProjectIcon.vue'; +import { LOADING_INDICATOR_TIMEOUT } from '@/features/mcpAccess/mcp.constants'; import { ElSwitch } from 'element-plus'; import { @@ -38,6 +39,7 @@ const rootStore = useRootStore(); const workflowsLoading = ref(false); const mcpStatusLoading = ref(false); +const mcpKeyLoading = ref(false); const availableWorkflows = ref([]); @@ -96,6 +98,8 @@ const tableActions = ref>>([ }, ]); +const apiKey = computed(() => mcpStore.currentUserMCPKey); + const isOwner = computed(() => usersStore.isInstanceOwner); const isAdmin = computed(() => usersStore.isAdmin); @@ -126,7 +130,7 @@ const fetchAvailableWorkflows = async () => { const workflows = await mcpStore.fetchWorkflowsAvailableForMCP(1, 200); availableWorkflows.value = workflows; } catch (error) { - toast.showError(error, 'Error fetching workflows'); + toast.showError(error, i18n.baseText('workflows.list.error.fetching')); } finally { workflowsLoading.value = false; } @@ -139,6 +143,7 @@ const onUpdateMCPEnabled = async (value: string | number | boolean) => { const updated = await mcpStore.setMcpAccessEnabled(boolValue); if (updated) { await fetchAvailableWorkflows(); + await fetchApiKey(); } else { workflowsLoading.value = false; } @@ -165,9 +170,39 @@ const onWorkflowAction = async (action: string, workflow: WorkflowListItem) => { } }; +const fetchApiKey = async () => { + try { + mcpKeyLoading.value = true; + await mcpStore.getOrCreateApiKey(); + } catch (error) { + toast.showError(error, i18n.baseText('settings.mcp.error.fetching.apiKey')); + } finally { + setTimeout(() => { + mcpKeyLoading.value = false; + }, LOADING_INDICATOR_TIMEOUT); + } +}; + +const rotateKey = async () => { + try { + mcpKeyLoading.value = true; + await mcpStore.generateNewApiKey(); + } catch (error) { + toast.showError(error, i18n.baseText('settings.mcp.error.rotating.apiKey')); + } finally { + setTimeout(() => { + mcpKeyLoading.value = false; + }, LOADING_INDICATOR_TIMEOUT); + } +}; + onMounted(async () => { documentTitle.set(i18n.baseText('settings.mcp')); - if (mcpStore.mcpAccessEnabled) await fetchAvailableWorkflows(); + if (!mcpStore.mcpAccessEnabled) { + return; + } + await fetchAvailableWorkflows(); + await fetchApiKey(); });