diff --git a/packages/frontend/editor-ui/src/features/mcpAccess/components/ConnectionParameter.vue b/packages/frontend/editor-ui/src/features/mcpAccess/components/ConnectionParameter.vue
new file mode 100644
index 00000000000..c327e9dce45
--- /dev/null
+++ b/packages/frontend/editor-ui/src/features/mcpAccess/components/ConnectionParameter.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
{{ props.value }}
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/features/mcpAccess/components/MCPConnectionInstructions.vue b/packages/frontend/editor-ui/src/features/mcpAccess/components/MCPConnectionInstructions.vue
index 7ce1669b27a..cecce694dc3 100644
--- a/packages/frontend/editor-ui/src/features/mcpAccess/components/MCPConnectionInstructions.vue
+++ b/packages/frontend/editor-ui/src/features/mcpAccess/components/MCPConnectionInstructions.vue
@@ -5,11 +5,15 @@ import { useI18n } from '@n8n/i18n';
import {
N8nButton,
N8nInfoAccordion,
- N8nLink,
+ N8nInfoTip,
+ N8nLoading,
N8nMarkdown,
+ N8nNotice,
N8nText,
N8nTooltip,
} from '@n8n/design-system';
+import type { ApiKey } from '@n8n/api-types';
+import ConnectionParameter from './ConnectionParameter.vue';
const MCP_ENDPOINT = 'mcp-server/http';
// TODO: Update once docs page is ready
@@ -17,10 +21,16 @@ const DOCS_URL = 'https://docs.n8n.io/';
type Props = {
baseUrl: string;
+ apiKey: ApiKey;
+ loadingApiKey: boolean;
};
const props = defineProps
();
+const emit = defineEmits<{
+ rotateKey: [];
+}>();
+
const { copy, copied, isSupported } = useClipboard();
const i18n = useI18n();
@@ -37,7 +47,7 @@ const connectionString = computed(() => {
"--streamableHttp",
"${props.baseUrl}${MCP_ENDPOINT}",
"--header",
- "authorization:Bearer "
+ "authorization:Bearer ${apiKeyText.value}"
]
}
}
@@ -45,6 +55,10 @@ const connectionString = computed(() => {
`;
});
+const isKeyRedacted = computed(() => {
+ return props.apiKey.apiKey.includes('******');
+});
+
// formatted code block for markdown component
const connectionCode = computed(() => {
return `\`\`\`json${connectionString.value}\`\`\``;
@@ -53,55 +67,74 @@ const connectionCode = computed(() => {
const fullServerUrl = computed(() => {
return props.baseUrl + MCP_ENDPOINT;
});
+
+const apiKeyText = computed(() => {
+ if (props.loadingApiKey) {
+ return `<${i18n.baseText('generic.loading')}...>`;
+ }
+ return isKeyRedacted.value ? '' : props.apiKey.apiKey;
+});
-
- -
-
-
- {{ i18n.baseText('settings.mcp.instructions.enableAccess') }}
-
-
-
- -
-
-
- {{ i18n.baseText('settings.mcp.instructions.serverUrl') }}:
-
-
- {{ fullServerUrl }}
-
+
+ -
+
+
+ {{ i18n.baseText('settings.mcp.instructions.enableAccess') }}
+
+
+
+ -
+
+
+ {{ i18n.baseText('settings.mcp.instructions.serverUrl') }}:
+
+
+
+
+ -
+
+
+ {{ i18n.baseText('settings.mcp.instructions.apiKey.label') }}:
+
+
+
-
-
-
-
-
-
-
- -
-
-
- {{ i18n.baseText('settings.mcp.instructions.apiKey.part1') }}
- {{ i18n.baseText('generic.apiKey') }}.
- {{ i18n.baseText('settings.mcp.instructions.apiKey.part2') }}
-
-
-
-
+
+
+
+
+
+
+
+ {{ i18n.baseText('settings.mcp.instructions.apiKey.tip') }}
+
+
+
+
+
+
@@ -111,7 +144,7 @@ const fullServerUrl = computed(() => {
:content="copied ? i18n.baseText('generic.copied') : i18n.baseText('generic.copy')"
>
{
flex-direction: column;
}
+.instructions-container {
+ :global(.notice) {
+ margin: var(--spacing-s) var(--spacing-l) var(--spacing-m);
+ }
+}
+
.instructions {
display: flex;
flex-direction: column;
@@ -144,14 +183,25 @@ const fullServerUrl = computed(() => {
padding-left: var(--spacing-l);
margin: var(--spacing-s);
+ li {
+ min-height: var(--spacing-l);
+ }
+
.item {
display: flex;
align-items: center;
gap: var(--spacing-2xs);
+
+ :global(.n8n-loading) div {
+ height: 32px;
+ width: 300px;
+ margin: 0;
+ }
}
.label {
font-size: var(--font-size-s);
+ flex: none;
}
.url {
diff --git a/packages/frontend/editor-ui/src/features/mcpAccess/mcp.api.ts b/packages/frontend/editor-ui/src/features/mcpAccess/mcp.api.ts
index d646c3817e2..088b2af924e 100644
--- a/packages/frontend/editor-ui/src/features/mcpAccess/mcp.api.ts
+++ b/packages/frontend/editor-ui/src/features/mcpAccess/mcp.api.ts
@@ -1,3 +1,4 @@
+import type { ApiKey } from '@n8n/api-types';
import type { IRestApiContext } from '@n8n/rest-api-client';
import { makeRestApiRequest } from '@n8n/rest-api-client';
@@ -17,3 +18,11 @@ export async function updateMcpSettings(
mcpAccessEnabled: enabled,
});
}
+
+export async function fetchApiKey(context: IRestApiContext): Promise {
+ return await makeRestApiRequest(context, 'GET', '/mcp/api-key');
+}
+
+export async function rotateApiKey(context: IRestApiContext): Promise {
+ return await makeRestApiRequest(context, 'POST', '/mcp/api-key/rotate');
+}
diff --git a/packages/frontend/editor-ui/src/features/mcpAccess/mcp.constants.ts b/packages/frontend/editor-ui/src/features/mcpAccess/mcp.constants.ts
index a67a40b55d0..16cfee07acd 100644
--- a/packages/frontend/editor-ui/src/features/mcpAccess/mcp.constants.ts
+++ b/packages/frontend/editor-ui/src/features/mcpAccess/mcp.constants.ts
@@ -1,3 +1,5 @@
export const MCP_SETTINGS_VIEW = 'McpSettings';
export const MCP_STORE = 'mcp';
+
+export const LOADING_INDICATOR_TIMEOUT = 200;
diff --git a/packages/frontend/editor-ui/src/features/mcpAccess/mcp.store.ts b/packages/frontend/editor-ui/src/features/mcpAccess/mcp.store.ts
index a8a091210be..6c8ece12f55 100644
--- a/packages/frontend/editor-ui/src/features/mcpAccess/mcp.store.ts
+++ b/packages/frontend/editor-ui/src/features/mcpAccess/mcp.store.ts
@@ -3,15 +3,19 @@ import { MCP_STORE } from './mcp.constants';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { WorkflowListItem } from '@/Interface';
import { useRootStore } from '@n8n/stores/useRootStore';
-import { updateMcpSettings } from '@/features/mcpAccess/mcp.api';
-import { computed } from 'vue';
+import { fetchApiKey, updateMcpSettings, rotateApiKey } from '@/features/mcpAccess/mcp.api';
+import { computed, ref } from 'vue';
import { useSettingsStore } from '@/stores/settings.store';
import { isWorkflowListItem } from '@/utils/typeGuards';
+import type { ApiKey } from '@n8n/api-types';
export const useMCPStore = defineStore(MCP_STORE, () => {
const workflowsStore = useWorkflowsStore();
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
+
+ const currentUserMCPKey = ref(null);
+
const mcpAccessEnabled = computed(() => !!settingsStore.moduleSettings.mcp?.mcpAccessEnabled);
async function fetchWorkflowsAvailableForMCP(
@@ -42,9 +46,24 @@ export const useMCPStore = defineStore(MCP_STORE, () => {
return updated;
}
+ async function getOrCreateApiKey(): Promise {
+ const apiKey = await fetchApiKey(rootStore.restApiContext);
+ currentUserMCPKey.value = apiKey;
+ return apiKey;
+ }
+
+ async function generateNewApiKey(): Promise {
+ const apiKey = await rotateApiKey(rootStore.restApiContext);
+ currentUserMCPKey.value = apiKey;
+ return apiKey;
+ }
+
return {
mcpAccessEnabled,
fetchWorkflowsAvailableForMCP,
setMcpAccessEnabled,
+ currentUserMCPKey,
+ getOrCreateApiKey,
+ generateNewApiKey,
};
});
diff --git a/packages/frontend/editor-ui/src/stores/ui.store.ts b/packages/frontend/editor-ui/src/stores/ui.store.ts
index 6ed93fb73de..3eb7b0f7988 100644
--- a/packages/frontend/editor-ui/src/stores/ui.store.ts
+++ b/packages/frontend/editor-ui/src/stores/ui.store.ts
@@ -382,7 +382,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
const items: IMenuItem[] = [];
Object.entries(registeredSettingsPages.value).forEach(([moduleName, moduleItems]) => {
if (settingsStore.isModuleActive(moduleName)) {
- items.push(...moduleItems);
+ items.push(...moduleItems.map((item) => ({ ...item, available: true })));
}
});
return items;