refactor(editor): Extract MCP access into a frontend module package (no-changelog) (#39493)

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Alex Grozav
2026-10-06 14:31:21 +00:00
committed by GitHub
co-authored by Claude Opus 5.5 cubic-dev-ai[bot]
parent 8924a11fda
commit 24dda0d435
111 changed files with 599 additions and 210 deletions
+1
View File
@@ -120,6 +120,7 @@ packages/cli/src/chat/ @n8n-io/ai
packages/cli/src/tool-generation/ @n8n-io/ai
packages/cli/src/modules/workflow-builder/ @n8n-io/ai-assistant
packages/cli/src/modules/mcp/ @n8n-io/adore
packages/modules/mcp/ @n8n-io/adore
packages/cli/src/modules/quick-connect/ @n8n-io/ai
packages/cli/src/modules/chat-hub/ @n8n-io/ai-assistant
packages/cli/src/modules/instance-ai/ @n8n-io/ai-assistant
@@ -44,6 +44,7 @@ export const sourcePackages = [
* The module tsconfig base holds that boundary.
*/
export const modulePackages: Array<{ name: string; dir: string; entry?: boolean }> = [
{ name: '@n8n/frontend-module-mcp', dir: 'modules/mcp/frontend' },
{
name: '@n8n/frontend-module-type-availability-policies',
dir: 'modules/type-availability-policies/frontend',
@@ -44,6 +44,11 @@ const extractedFeatures = [
message:
'insights is the @n8n/frontend-module-insights package. The shell registers a module through src/app/modules.manifest.ts.',
},
{
group: ['@/features/ai/mcpAccess', '@/features/ai/mcpAccess/*'],
message:
'mcpAccess is the @n8n/frontend-module-mcp package. The shell registers a module through src/app/modules.manifest.ts.',
},
];
export default defineConfig(
+1
View File
@@ -56,6 +56,7 @@
"@n8n/frontend-constants": "workspace:*",
"@n8n/frontend-module-insights": "workspace:*",
"@n8n/frontend-module-instance-registry": "workspace:*",
"@n8n/frontend-module-mcp": "workspace:*",
"@n8n/frontend-module-otel": "workspace:*",
"@n8n/frontend-module-sdk": "workspace:*",
"@n8n/frontend-module-type-availability-policies": "workspace:*",
@@ -20,7 +20,7 @@ import { useWorkflowActivate } from '@/app/composables/useWorkflowActivate';
import { createTestingPinia } from '@pinia/testing';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { useUsersStore } from '@n8n/stores/users.store';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMCPStore } from '@n8n/frontend-module-mcp';
import { useUIStore } from '@/app/stores/ui.store';
import { SURFACE_MCP_ONBOARDING_MODAL_KEY } from '@/experiments/surfaceMcpToNewCloudUsers/constants';
// Experiment cleanup: remove with openWorkflowInAssistant.
@@ -51,8 +51,7 @@ import {
import OpenInAssistantCardButton from '@/experiments/openWorkflowInAssistant/components/OpenInAssistantCardButton.vue';
import { useOpenInAssistantCard } from '@/experiments/openWorkflowInAssistant/composables/useOpenInAssistantCard';
import WorkflowCardMcpToggle from '@/app/components/WorkflowCardMcpToggle.vue';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useMCPStore, useMcp } from '@n8n/frontend-module-mcp';
import { useWorkflowActivate } from '@/app/composables/useWorkflowActivate';
import { createEventBus } from '@n8n/utils/event-bus';
import { usePrivateCredentials } from '@/features/resolvers/composables/usePrivateCredentials';
@@ -4,8 +4,7 @@ import { N8nIcon, N8nSwitch2, N8nTooltip } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
import { useToast } from '@n8n/composables/useToast';
import { useUIStore } from '@/app/stores/ui.store';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useMCPStore, useMcp } from '@n8n/frontend-module-mcp';
import { SURFACE_MCP_ONBOARDING_MODAL_KEY } from '@/experiments/surfaceMcpToNewCloudUsers/constants';
const props = defineProps<{
@@ -31,7 +31,7 @@ import type { INodeTypeDescription } from 'n8n-workflow';
import { createTestNode } from '@/__tests__/mocks';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { useUsersStore } from '@n8n/stores/users.store';
import { MCP_DOCS_PAGE_URL, MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import { MCP_DOCS_PAGE_URL, MCP_SETTINGS_VIEW } from '@n8n/frontend-module-mcp';
vi.mock('vue-router', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-router')>();
@@ -49,8 +49,6 @@ vi.mock('@n8n/composables/useTelemetry', () => ({
useTelemetry: vi.fn(),
}));
vi.mock('@/features/ai/mcpAccess/composables/useMcp', () => ({}));
const mockEvaluationSetOutputsNodeExist = ref(false);
vi.mock('@/features/ai/evaluation.ee/composables/useWorkflowEvaluationState', () => ({
useWorkflowEvaluationState: () => ({
@@ -19,7 +19,7 @@ import {
} from '@/app/constants';
import { useTelemetry } from '@n8n/composables/useTelemetry';
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
import { MCP_DOCS_PAGE_URL, MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import { MCP_DOCS_PAGE_URL, MCP_SETTINGS_VIEW } from '@n8n/frontend-module-mcp';
import { N8nSuggestedActions } from '@n8n/design-system';
import { useSettingsStore } from '@n8n/stores/settings.store';
@@ -21,6 +21,7 @@ const EXTRACTED_MODULES = [
{ group: '@/features/instanceRegistry', package: '@n8n/frontend-module-instance-registry' },
{ group: '@/features/settings/otel', package: '@n8n/frontend-module-otel' },
{ group: '@/features/execution/insights', package: '@n8n/frontend-module-insights' },
{ group: '@/features/ai/mcpAccess', package: '@n8n/frontend-module-mcp' },
];
// One representative path per block in `eslint.config.mjs` that sets the rule, plus
@@ -1,6 +1,6 @@
import type { FrontendModuleDescription } from '@n8n/frontend-module-sdk';
import { DataTableModule } from '@/features/core/dataTable/module.descriptor';
import { MCPModule } from '@/features/ai/mcpAccess/module.descriptor';
import { MCPModule } from '@n8n/frontend-module-mcp/mcp.module';
import { ChatModule } from '@/features/ai/chatHub/module.descriptor';
import { InstanceAiModule } from '@/features/ai/instanceAi/module.descriptor';
import { AgentsModule } from '@/features/agents/module.descriptor';
@@ -16,7 +16,7 @@ import {
N8nText,
N8nTooltip,
} from '@n8n/design-system';
import { MCP_SCOPE_GROUPS } from '@/features/ai/mcpAccess/mcp.constants';
import { MCP_SCOPE_GROUPS } from '@n8n/frontend-module-mcp';
import { useToast } from '@n8n/composables/useToast';
import { useTelemetry } from '@n8n/composables/useTelemetry';
import ScopesSelector from '@/app/components/scopes/ScopesSelector.vue';
@@ -80,9 +80,11 @@ import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useEnvironmentsStore } from '@/features/settings/environments.ee/environments.store';
import { useDataTableStore } from '@/features/core/dataTable/dataTable.store';
import { MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import type { ToggleWorkflowsMcpAccessResponse } from '@/features/ai/mcpAccess/mcp.api';
import {
MCP_SETTINGS_VIEW,
useMCPStore,
type ToggleWorkflowsMcpAccessResponse,
} from '@n8n/frontend-module-mcp';
import {
type Project,
type ProjectSharingData,
@@ -2,7 +2,7 @@ import { createComponentRenderer } from '@/__tests__/render';
import { mockedStore, type MockedStore } from '@/__tests__/utils';
import { useToast } from '@n8n/composables/useToast';
import { useExposeAllWorkflowsToMcpStore } from '@/experiments/exposeAllWorkflowsToMcp/stores/exposeAllWorkflowsToMcp.store';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMCPStore } from '@n8n/frontend-module-mcp';
import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
import { defineComponent } from 'vue';
@@ -12,7 +12,8 @@ const { trackAutoExposeToggledSpy } = vi.hoisted(() => ({
trackAutoExposeToggledSpy: vi.fn(),
}));
vi.mock('@/features/ai/mcpAccess/composables/useMcp', () => ({
vi.mock('@n8n/frontend-module-mcp', async (importOriginal) => ({
...(await importOriginal()),
useMcp: () => ({ trackAutoExposeToggled: trackAutoExposeToggledSpy }),
}));
@@ -4,8 +4,7 @@ import { useToast } from '@n8n/composables/useToast';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { EXPOSE_ALL_WORKFLOWS_TO_MCP_MODAL_KEY } from '@/experiments/exposeAllWorkflowsToMcp/constants';
import { useExposeAllWorkflowsToMcpStore } from '@/experiments/exposeAllWorkflowsToMcp/stores/exposeAllWorkflowsToMcp.store';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useMCPStore, useMcp } from '@n8n/frontend-module-mcp';
import { N8nButton, N8nText } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
import { createEventBus } from '@n8n/utils/event-bus';
@@ -5,7 +5,7 @@ import { mockedStore, type MockedStore } from '@/__tests__/utils';
import { useUIStore } from '@/app/stores/ui.store';
import { EXPOSE_ALL_WORKFLOWS_TO_MCP_MODAL_KEY } from '@/experiments/exposeAllWorkflowsToMcp/constants';
import { useExposeAllWorkflowsToMcpStore } from '@/experiments/exposeAllWorkflowsToMcp/stores/exposeAllWorkflowsToMcp.store';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMCPStore } from '@n8n/frontend-module-mcp';
import { useExposeAllWorkflowsToMcpOffer } from './useExposeAllWorkflowsToMcpOffer';
@@ -2,7 +2,7 @@ import { useSettingsStore } from '@n8n/stores/settings.store';
import { useUIStore } from '@/app/stores/ui.store';
import { EXPOSE_ALL_WORKFLOWS_TO_MCP_MODAL_KEY } from '@/experiments/exposeAllWorkflowsToMcp/constants';
import { useExposeAllWorkflowsToMcpStore } from '@/experiments/exposeAllWorkflowsToMcp/stores/exposeAllWorkflowsToMcp.store';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMCPStore } from '@n8n/frontend-module-mcp';
export function useExposeAllWorkflowsToMcpOffer() {
const experimentStore = useExposeAllWorkflowsToMcpStore();
@@ -1,7 +1,7 @@
import { createComponentRenderer } from '@/__tests__/render';
import type { McpJsonNudgeAction } from '@/experiments/mcpJsonNudge/composables/useMcpJsonNudgeTrigger';
import type { McpJsonNudgeSurface } from '@/experiments/mcpJsonNudge/constants';
import { MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import { MCP_SETTINGS_VIEW } from '@n8n/frontend-module-mcp';
import { TELEMETRY_EVENT } from '@n8n/telemetry';
import userEvent from '@testing-library/user-event';
import type { Mock } from 'vitest';
@@ -2,8 +2,7 @@
import Modal from '@/app/components/Modal.vue';
import { useMcpJsonNudgeEligibility } from '@/experiments/mcpJsonNudge/composables/useMcpJsonNudgeEligibility';
import type { McpJsonNudgeAction } from '@/experiments/mcpJsonNudge/composables/useMcpJsonNudgeTrigger';
import McpClientLogoCards from '@/features/ai/mcpAccess/components/McpClientLogoCards.vue';
import { MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import { McpClientLogoCards, MCP_SETTINGS_VIEW } from '@n8n/frontend-module-mcp';
import { useTelemetry } from '@n8n/composables/useTelemetry';
import { N8nButton, N8nCheckbox, N8nText } from '@n8n/design-system';
import { type BaseTextKey, useI18n } from '@n8n/i18n';
@@ -8,7 +8,8 @@ const mockSetCalloutDismissed = vi.hoisted(() => vi.fn());
const mockCurrentUser = vi.hoisted<{ settings: Record<string, unknown> }>(() => ({ settings: {} }));
const mockUpdateUserSettings = vi.hoisted(() => vi.fn());
vi.mock('@/features/ai/mcpAccess/mcp.store', () => ({
vi.mock('@n8n/frontend-module-mcp', async (importOriginal) => ({
...(await importOriginal()),
useMCPStore: () => mockMcpStore,
}));
@@ -3,7 +3,7 @@ import { useUsersStore } from '@n8n/stores/users.store';
import { MCP_JSON_NUDGE_EXPERIMENT } from '@/app/constants/experiments';
import { usePostHog } from '@/app/stores/posthog.store';
import { MCP_JSON_NUDGE_CALLOUT } from '@/experiments/mcpJsonNudge/constants';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMCPStore } from '@n8n/frontend-module-mcp';
const IMPRESSION_CAP = 2;
@@ -2,7 +2,7 @@ import { createComponentRenderer } from '@/__tests__/render';
import { mockedStore } from '@/__tests__/utils';
import { useUIStore } from '@/app/stores/ui.store';
import { SURFACE_MCP_TO_NEW_CLOUD_USERS_EXPERIMENT } from '@/app/constants/experiments';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMCPStore } from '@n8n/frontend-module-mcp';
import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
import { useSurfaceMcpToNewCloudUsersStore } from '../stores/surfaceMcpToNewCloudUsers.store';
@@ -4,7 +4,7 @@ import { N8nCard, N8nIcon, N8nText } from '@n8n/design-system';
import { useI18n, type BaseTextKey } from '@n8n/i18n';
import { SURFACE_MCP_TO_NEW_CLOUD_USERS_EXPERIMENT } from '@/app/constants/experiments';
import { useUIStore } from '@/app/stores/ui.store';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMCPStore } from '@n8n/frontend-module-mcp';
import { SURFACE_MCP_ONBOARDING_MODAL_KEY } from '../constants';
import { useSurfaceMcpToNewCloudUsersStore } from '../stores/surfaceMcpToNewCloudUsers.store';
import SurfaceMcpTileLogos from './SurfaceMcpTileLogos.vue';
@@ -51,7 +51,8 @@ type MockMcpStore = {
let mockMcpStore: MockMcpStore;
vi.mock('@/features/ai/mcpAccess/mcp.store', () => ({
vi.mock('@n8n/frontend-module-mcp', async (importOriginal) => ({
...(await importOriginal()),
useMCPStore: () => mockMcpStore,
}));
@@ -4,9 +4,11 @@ import { useToast } from '@n8n/composables/useToast';
import SurfaceMcpBridgeGraphic from '@/experiments/surfaceMcpToNewCloudUsers/components/SurfaceMcpBridgeGraphic.vue';
import { SURFACE_MCP_ONBOARDING_MODAL_KEY } from '@/experiments/surfaceMcpToNewCloudUsers/constants';
import { useSurfaceMcpToNewCloudUsersStore } from '@/experiments/surfaceMcpToNewCloudUsers/stores/surfaceMcpToNewCloudUsers.store';
import MCPAccessToggle from '@/features/ai/mcpAccess/components/McpAccessToggle.vue';
import { MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import {
McpAccessToggle as MCPAccessToggle,
MCP_SETTINGS_VIEW,
useMCPStore,
} from '@n8n/frontend-module-mcp';
import { N8nIcon, N8nLink, N8nText } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
import type { BaseTextKey } from '@n8n/i18n';
@@ -10,7 +10,8 @@ const mocks = vi.hoisted(() => ({
trackOpportunityViewed: vi.fn(),
}));
vi.mock('@/features/ai/mcpAccess/mcp.store', () => ({
vi.mock('@n8n/frontend-module-mcp', async (importOriginal) => ({
...(await importOriginal()),
useMCPStore: () => ({
get mcpAccessEnabled() {
return mocks.mcpAccessEnabled;
@@ -1,4 +1,4 @@
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMCPStore } from '@n8n/frontend-module-mcp';
import { computed, ref, watch, type Ref } from 'vue';
import { useSurfaceMcpToNewCloudUsersStore } from '../stores/surfaceMcpToNewCloudUsers.store';
import { useSurfaceMcpToNewCloudUsersEligibility } from './useSurfaceMcpToNewCloudUsersEligibility';
@@ -2361,7 +2361,7 @@ describe('AgentBuilderView — preview routing', { timeout: 60_000 }, () => {
updatedIds: string[];
unchangedIds: string[];
}>();
const { useMCPStore } = await import('@/features/ai/mcpAccess/mcp.store');
const { useMCPStore } = await import('@n8n/frontend-module-mcp');
const toggleAgentMcpAccess = vi
.spyOn(useMCPStore(), 'toggleAgentMcpAccess')
.mockReturnValueOnce(mcpSave.promise);
@@ -3865,7 +3865,7 @@ describe('AgentBuilderView — three-column shell', () => {
artifactEditingLocked: false,
},
});
const { useMCPStore } = await import('@/features/ai/mcpAccess/mcp.store');
const { useMCPStore } = await import('@n8n/frontend-module-mcp');
const toggleAgentMcpAccess = vi
.spyOn(useMCPStore(), 'toggleAgentMcpAccess')
.mockResolvedValue({ updatedCount: 1, updatedIds: ['a2'], unchangedIds: [] });
@@ -3898,7 +3898,7 @@ describe('AgentBuilderView — three-column shell', () => {
artifactEditingLocked: false,
},
});
const { useMCPStore } = await import('@/features/ai/mcpAccess/mcp.store');
const { useMCPStore } = await import('@n8n/frontend-module-mcp');
vi.spyOn(useMCPStore(), 'toggleAgentMcpAccess').mockResolvedValue({
updatedCount: 1,
updatedIds: ['a2'],
@@ -3942,7 +3942,7 @@ describe('AgentBuilderView — three-column shell', () => {
artifactAgentId: 'a1',
},
});
const { useMCPStore } = await import('@/features/ai/mcpAccess/mcp.store');
const { useMCPStore } = await import('@n8n/frontend-module-mcp');
const toggleAgentMcpAccess = vi.spyOn(useMCPStore(), 'toggleAgentMcpAccess').mockResolvedValue({
updatedCount: 1,
updatedIds: ['a1'],
@@ -4017,7 +4017,7 @@ describe('AgentBuilderView — three-column shell', () => {
},
});
const header = wrapper.findComponent({ name: 'AgentBuilderHeader' });
const { useMCPStore } = await import('@/features/ai/mcpAccess/mcp.store');
const { useMCPStore } = await import('@n8n/frontend-module-mcp');
vi.spyOn(useMCPStore(), 'toggleAgentMcpAccess').mockRejectedValue(new Error('mcp save failed'));
vi.useFakeTimers();
@@ -5755,7 +5755,7 @@ describe(
};
const wrapper = await renderView();
await flushPromises();
const { useMCPStore } = await import('@/features/ai/mcpAccess/mcp.store');
const { useMCPStore } = await import('@n8n/frontend-module-mcp');
vi.spyOn(useMCPStore(), 'toggleAgentMcpAccess').mockRejectedValue(
new Error('mcp save failed'),
);
@@ -63,13 +63,11 @@ const mcpStoreMock = {
toggleAgentMcpAccess: vi.fn().mockResolvedValue({ updatedCount: 1 }),
};
vi.mock('@/features/ai/mcpAccess/mcp.store', () => ({
useMCPStore: () => mcpStoreMock,
}));
const trackMcpAccessEnabledForAgentMock = vi.fn();
vi.mock('@/features/ai/mcpAccess/composables/useMcp', () => ({
vi.mock('@n8n/frontend-module-mcp', async (importOriginal) => ({
...(await importOriginal()),
useMCPStore: () => mcpStoreMock,
useMcp: () => ({ trackMcpAccessEnabledForAgent: trackMcpAccessEnabledForAgentMock }),
}));
@@ -17,8 +17,7 @@ import PublicationIndicator from '@/app/components/PublicationIndicator.vue';
import TimeAgo from '@/app/components/TimeAgo.vue';
import { useToast } from '@n8n/composables/useToast';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMcp, useMCPStore } from '@n8n/frontend-module-mcp';
import { deleteAgent } from '../composables/useAgentApi';
import { useAgentConfirmationModal } from '../composables/useAgentConfirmationModal';
import { useAgentPermissions } from '../composables/useAgentPermissions';
@@ -136,8 +136,7 @@ import InstanceAiChatPanel from '@/features/ai/instanceAi/embed/InstanceAiChatPa
import { persistPendingAgent } from '@/features/ai/instanceAi/instanceAi.memory.api';
import type { InstanceAiEmbedSubject } from '@/features/ai/instanceAi/embed/instanceAiEmbed.types';
import AgentBuildingIndicator from '@/features/ai/instanceAi/components/AgentBuildingIndicator.vue';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMcp, useMCPStore } from '@n8n/frontend-module-mcp';
import { useAgentCollaborationStore } from '../stores/agentCollaboration.store';
import { useActivityDetection } from '@/app/composables/useActivityDetection';
import { buildAgentChangeRequestPrompt } from '../utils/agent-change-request';
@@ -57,7 +57,7 @@ import { useI18n } from '@n8n/i18n';
import { useTelemetry } from '@n8n/composables/useTelemetry';
import { useDebounce } from '@n8n/composables/useDebounce';
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useMcp } from '@n8n/frontend-module-mcp';
import RedactionMembersModal from '@/features/workflows/components/WorkflowSettings/RedactionMembersModal.vue';
import { useGlobalLinkActions } from '@/app/composables/useGlobalLinkActions';
import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper';
@@ -25,6 +25,8 @@
"@n8n/composables*": ["../@n8n/composables/src*"],
"@n8n/constants*": ["../../@n8n/constants/src*"],
"@n8n/frontend-module-sdk": ["../@n8n/frontend-module-sdk/src/index.ts"],
"@n8n/frontend-module-mcp": ["../../modules/mcp/frontend/src/index.ts"],
"@n8n/frontend-module-mcp/*": ["../../modules/mcp/frontend/src/*"],
"@n8n/frontend-module-type-availability-policies": [
"../../modules/type-availability-policies/frontend/src/index.ts"
],
+51
View File
@@ -0,0 +1,51 @@
# @n8n/frontend-module-mcp
Frontend feature module. Consumed from source by the editor-ui shell through
`src/app/modules.manifest.ts`; there is no build step and no `dist`.
Read the full guide at `packages/@n8n/module-cli/frontend-module-guide.md`. It gives the
descriptor contract, the registration points and the known problems. This README gives a
summary only.
```bash
pnpm turbo typecheck --filter=@n8n/frontend-module-mcp
pnpm turbo lint --filter=@n8n/frontend-module-mcp
pnpm turbo lint:styles --filter=@n8n/frontend-module-mcp
pnpm turbo test --filter=@n8n/frontend-module-mcp
```
Go through turbo, not `pnpm --filter <pkg> typecheck`: this package is consumed
from source, and on a cold tree its platform dependencies have not been built
yet. Turbo builds them first; the bare pnpm form does not.
## Import rules
- Depend on L0–L2 platform packages only. The full list is in "Imports and
boundaries" in the guide. Never import another `@n8n/frontend-module-*`, and
never import `@/…` from the shell.
- `@n8n/stores`, `@n8n/composables`, `@n8n/frontend-constants` and
`@n8n/utils` are **subpath-only** — import `@n8n/stores/settings.store`, not
`@n8n/stores`.
- The no-cross-module rule is currently a convention: the shared tsconfig base
omits sibling modules from `paths`, which blocks an accidental import but not
a deliberate one (declaring the dependency makes it typecheck clean). The
CAT-3692 tracks the ESLint rule that will enforce it.
## Adding UI
`vite.config.ts` already carries the three plugins a design-system consumer
needs, so a `.vue` file compiles in tests without further setup:
- `@vitejs/plugin-vue` — single-file components.
- `unplugin-icons` — the `~icons/lucide/*` virtual modules behind `N8nIcon`.
- `vite-svg-loader` — design-system's `custom/*.svg` icon components. Without
it an `.svg` import is a data-URI string, which Vue renders as a tag name.
`src/__tests__/design-system-icons.test.ts` renders one icon of each kind, so a
plugin dropped from `vite.config.ts` fails this package's own suite. Keep that
test while the module renders any design-system component.
SCSS goes in a scoped `<style lang="scss">` block, with tokens from
design-system — `stylelint.config.mjs` holds this package to the same rules the
shell uses. Route components must load lazily — see the note in
`src/mcp.module.ts`.
@@ -0,0 +1,4 @@
{
"$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["../../../../biome.jsonc"]
}
@@ -0,0 +1,4 @@
import { defineConfig } from 'eslint/config';
import { frontendConfig } from '@n8n/eslint-config/frontend';
export default defineConfig(frontendConfig);
@@ -0,0 +1,72 @@
{
"name": "@n8n/frontend-module-mcp",
"version": "0.1.0",
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./mcp.module": "./src/mcp.module.ts"
},
"scripts": {
"clean": "rimraf .turbo",
"typecheck": "vue-tsc --noEmit",
"test": "vitest run",
"test:changed": "janitor test-scoped",
"test:dev": "vitest",
"lint": "eslint src --quiet",
"lint:fix": "eslint src --fix",
"lint:styles": "stylelint \"src/**/*.{scss,sass,vue}\" --cache --allow-empty-input",
"lint:styles:fix": "stylelint \"src/**/*.{scss,sass,vue}\" --fix --cache --allow-empty-input",
"format": "biome format --write . && prettier --write . --ignore-path ../../../../.prettierignore",
"format:check": "biome ci . && prettier --check . --ignore-path ../../../../.prettierignore"
},
"dependencies": {
"@n8n/api-types": "workspace:*",
"@n8n/composables": "workspace:*",
"@n8n/design-system": "workspace:*",
"@n8n/frontend-constants": "workspace:*",
"@n8n/frontend-module-sdk": "workspace:*",
"@n8n/i18n": "workspace:*",
"@n8n/permissions": "workspace:*",
"@n8n/rest-api-client": "workspace:*",
"@n8n/stores": "workspace:*",
"@n8n/telemetry": "workspace:*",
"@n8n/utils": "workspace:*",
"element-plus": "catalog:frontend",
"lodash": "catalog:",
"n8n-workflow": "workspace:*",
"pinia": "catalog:frontend",
"reka-ui": "^2.5.0",
"vue": "catalog:frontend",
"vue-i18n": "catalog:frontend",
"vue-router": "catalog:frontend"
},
"devDependencies": {
"@iconify/json": "catalog:",
"@n8n/eslint-config": "workspace:*",
"@n8n/frontend-test-utils": "workspace:*",
"@n8n/frontend-vite-config": "workspace:*",
"@n8n/playwright-janitor": "workspace:*",
"@n8n/stylelint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@pinia/testing": "^0.1.6",
"@testing-library/jest-dom": "catalog:frontend",
"@testing-library/user-event": "catalog:frontend",
"@testing-library/vue": "catalog:frontend",
"@types/lodash": "catalog:",
"@vitejs/plugin-vue": "catalog:frontend",
"@vitest/coverage-v8": "catalog:",
"eslint": "catalog:",
"jsdom": "catalog:",
"stylelint": "catalog:",
"typescript": "catalog:",
"unplugin-icons": "catalog:frontend",
"vite": "catalog:",
"vite-svg-loader": "catalog:frontend",
"vitest": "catalog:",
"vitest-mock-extended": "catalog:",
"vue-tsc": "catalog:frontend"
},
"license": "LicenseRef-n8n-sustainable-use"
}
@@ -3,12 +3,12 @@ import { createTestingPinia } from '@pinia/testing';
import { waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createComponentRenderer, mockedStore, type MockedStore } from '@n8n/frontend-test-utils';
import SettingsMCPAgentsView from '@/features/ai/mcpAccess/SettingsMCPAgentsView.vue';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import SettingsMCPAgentsView from './SettingsMCPAgentsView.vue';
import { useMCPStore } from './mcp.store';
import { useSettingsStore } from '@n8n/stores/settings.store';
import type { FrontendSettings } from '@n8n/api-types';
import { MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import type { McpAgent } from '@/features/ai/mcpAccess/mcp.types';
import { MCP_SETTINGS_VIEW } from './mcp.constants';
import type { McpAgent } from './mcp.types';
const { routerPush, routerReplace } = vi.hoisted(() => ({
routerPush: vi.fn(),
@@ -15,15 +15,11 @@ import { useTelemetry } from '@n8n/composables/useTelemetry';
import { TELEMETRY_EVENT } from '@n8n/telemetry';
import { useToast } from '@n8n/composables/useToast';
import { useSettingsStore } from '@n8n/stores/settings.store';
import type { McpAgent } from '@/features/ai/mcpAccess/mcp.types';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import {
LOADING_INDICATOR_TIMEOUT,
MCP_DOCS_PAGE_URL,
MCP_SETTINGS_VIEW,
} from '@/features/ai/mcpAccess/mcp.constants';
import AgentsTable from '@/features/ai/mcpAccess/components/tabs/AgentsTable.vue';
import MCPConnectAgentsModal from '@/features/ai/mcpAccess/modals/MCPConnectAgentsModal.vue';
import type { McpAgent } from './mcp.types';
import { useMCPStore } from './mcp.store';
import { LOADING_INDICATOR_TIMEOUT, MCP_DOCS_PAGE_URL, MCP_SETTINGS_VIEW } from './mcp.constants';
import AgentsTable from './components/tabs/AgentsTable.vue';
import MCPConnectAgentsModal from './modals/MCPConnectAgentsModal.vue';
const i18n = useI18n();
const toast = useToast();
@@ -3,15 +3,15 @@ import { createTestingPinia } from '@pinia/testing';
import { waitFor, within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createComponentRenderer, mockedStore, type MockedStore } from '@n8n/frontend-test-utils';
import SettingsMCPClientsView from '@/features/ai/mcpAccess/SettingsMCPClientsView.vue';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import SettingsMCPClientsView from './SettingsMCPClientsView.vue';
import { useMCPStore } from './mcp.store';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { useUsersStore } from '@n8n/stores/users.store';
import { useRBACStore } from '@n8n/stores/rbac.store';
import { mock } from 'vitest-mock-extended';
import type { IUser } from '@n8n/rest-api-client/api/users';
import type { FrontendSettings } from '@n8n/api-types';
import { MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import { MCP_SETTINGS_VIEW } from './mcp.constants';
import { TELEMETRY_EVENT } from '@n8n/telemetry';
const { routerPush, routerReplace, route } = vi.hoisted(() => ({
@@ -7,17 +7,13 @@ import { N8nSettingsLayout, N8nSettingsPageHeader } from '@n8n/design-system';
import { useDocumentTitle } from '@n8n/composables/useDocumentTitle';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { useToast } from '@n8n/composables/useToast';
import type { OAuthClientFilters } from '@/features/ai/mcpAccess/clients.utils';
import OAuthClientsTable from '@/features/ai/mcpAccess/components/tabs/OAuthClientsTable.vue';
import RevokeOAuthClientConfirmModal from '@/features/ai/mcpAccess/components/RevokeOAuthClientConfirmModal.vue';
import {
LOADING_INDICATOR_TIMEOUT,
MCP_DOCS_PAGE_URL,
MCP_SETTINGS_VIEW,
} from '@/features/ai/mcpAccess/mcp.constants';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useOAuthClientRevoke } from '@/features/ai/mcpAccess/composables/useOAuthClientRevoke';
import type { OAuthClientFilters } from './clients.utils';
import OAuthClientsTable from './components/tabs/OAuthClientsTable.vue';
import RevokeOAuthClientConfirmModal from './components/RevokeOAuthClientConfirmModal.vue';
import { LOADING_INDICATOR_TIMEOUT, MCP_DOCS_PAGE_URL, MCP_SETTINGS_VIEW } from './mcp.constants';
import { useMCPStore } from './mcp.store';
import { useMcp } from './composables/useMcp';
import { useOAuthClientRevoke } from './composables/useOAuthClientRevoke';
import { useRBACStore } from '@n8n/stores/rbac.store';
const i18n = useI18n();
@@ -11,16 +11,15 @@ import {
type MockedStore,
waitAllPromises,
} from '@n8n/frontend-test-utils';
import SettingsMCPView from '@/features/ai/mcpAccess/SettingsMCPView.vue';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import SettingsMCPView from './SettingsMCPView.vue';
import { useMCPStore } from './mcp.store';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { useRBACStore } from '@n8n/stores/rbac.store';
import type { FrontendSettings, OAuthClientResponseDto } from '@n8n/api-types';
import { MCP_CLIENTS_VIEW, MCP_WORKFLOWS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import type { McpAgent, McpWorkflow } from '@/features/ai/mcpAccess/mcp.types';
import { MCP_CLIENTS_VIEW, MCP_WORKFLOWS_VIEW, UNKNOWN_COUNT_VALUE } from './mcp.constants';
import type { McpAgent, McpWorkflow } from './mcp.types';
import { UNKNOWN_COUNT_VALUE } from '@/features/ai/mcpAccess/mcp.constants';
import { createOAuthClient } from '@/features/ai/mcpAccess/mcp.test.utils';
import { createOAuthClient } from './mcp.test.utils';
import { useToast } from '@n8n/composables/useToast';
const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() }));
@@ -65,7 +64,7 @@ vi.mock('@n8n/composables/useDocumentTitle', () => ({
}),
}));
vi.mock('@/features/ai/mcpAccess/composables/useMcp', () => ({
vi.mock('./composables/useMcp', () => ({
useMcp: () => ({
trackUserToggledMcpAccess: vi.fn(),
trackAutoExposeToggled: trackAutoExposeToggledSpy,
@@ -21,27 +21,26 @@ import {
import { useDocumentTitle } from '@n8n/composables/useDocumentTitle';
import { useToast } from '@n8n/composables/useToast';
import MCPEmptyState from '@/features/ai/mcpAccess/components/MCPEmptyState.vue';
import McpAllowedCallbackUrlsDialog from '@/features/ai/mcpAccess/components/McpAllowedCallbackUrlsDialog.vue';
import McpConnectClientDialog from '@/features/ai/mcpAccess/components/McpConnectClientDialog.vue';
import McpConnectedClientRow from '@/features/ai/mcpAccess/components/McpConnectedClientRow.vue';
import McpStatusControl from '@/features/ai/mcpAccess/components/McpStatusControl.vue';
import OAuthClientDetailsModal from '@/features/ai/mcpAccess/components/OAuthClientDetailsModal.vue';
import RevokeOAuthClientConfirmModal from '@/features/ai/mcpAccess/components/RevokeOAuthClientConfirmModal.vue';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useOAuthClientRevoke } from '@/features/ai/mcpAccess/composables/useOAuthClientRevoke';
import MCPEmptyState from './components/MCPEmptyState.vue';
import McpAllowedCallbackUrlsDialog from './components/McpAllowedCallbackUrlsDialog.vue';
import McpConnectClientDialog from './components/McpConnectClientDialog.vue';
import McpConnectedClientRow from './components/McpConnectedClientRow.vue';
import McpStatusControl from './components/McpStatusControl.vue';
import OAuthClientDetailsModal from './components/OAuthClientDetailsModal.vue';
import RevokeOAuthClientConfirmModal from './components/RevokeOAuthClientConfirmModal.vue';
import { useMcp } from './composables/useMcp';
import { useOAuthClientRevoke } from './composables/useOAuthClientRevoke';
import {
MCP_AGENTS_VIEW,
MCP_CLIENTS_VIEW,
MCP_DOCS_PAGE_URL,
MCP_WORKFLOWS_VIEW,
} from '@/features/ai/mcpAccess/mcp.constants';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
UNKNOWN_COUNT_VALUE,
} from './mcp.constants';
import { useMCPStore } from './mcp.store';
import { useSettingsStore } from '@n8n/stores/settings.store';
import { useRBACStore } from '@n8n/stores/rbac.store';
import { UNKNOWN_COUNT_VALUE } from '@/features/ai/mcpAccess/mcp.constants';
const i18n = useI18n();
const toast = useToast();
const documentTitle = useDocumentTitle({
@@ -4,14 +4,14 @@ import { waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { capabilities, capabilityRegistry } from '@n8n/frontend-module-sdk';
import { createComponentRenderer, mockedStore, type MockedStore } from '@n8n/frontend-test-utils';
import SettingsMCPWorkflowsView from '@/features/ai/mcpAccess/SettingsMCPWorkflowsView.vue';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import SettingsMCPWorkflowsView from './SettingsMCPWorkflowsView.vue';
import { useMCPStore } from './mcp.store';
import { useSettingsStore } from '@n8n/stores/settings.store';
import type { FrontendSettings } from '@n8n/api-types';
import { WORKFLOW_DESCRIPTION_MODAL_KEY } from '@n8n/frontend-constants/workflows';
import { MCP_SETTINGS_VIEW } from '@/features/ai/mcpAccess/mcp.constants';
import { createWorkflow } from '@/features/ai/mcpAccess/mcp.test.utils';
import type { McpWorkflow } from '@/features/ai/mcpAccess/mcp.types';
import { MCP_SETTINGS_VIEW } from './mcp.constants';
import { createWorkflow } from './mcp.test.utils';
import type { McpWorkflow } from './mcp.types';
const { routerPush, routerReplace } = vi.hoisted(() => ({
routerPush: vi.fn(),
@@ -16,15 +16,11 @@ import { useTelemetry } from '@n8n/composables/useTelemetry';
import { useToast } from '@n8n/composables/useToast';
import { WORKFLOW_DESCRIPTION_MODAL_KEY } from '@n8n/frontend-constants/workflows';
import { useSettingsStore } from '@n8n/stores/settings.store';
import type { McpWorkflow } from '@/features/ai/mcpAccess/mcp.types';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import {
LOADING_INDICATOR_TIMEOUT,
MCP_DOCS_PAGE_URL,
MCP_SETTINGS_VIEW,
} from '@/features/ai/mcpAccess/mcp.constants';
import WorkflowsTable from '@/features/ai/mcpAccess/components/tabs/WorkflowsTable.vue';
import MCPConnectWorkflowsModal from '@/features/ai/mcpAccess/modals/MCPConnectWorkflowsModal.vue';
import type { McpWorkflow } from './mcp.types';
import { useMCPStore } from './mcp.store';
import { LOADING_INDICATOR_TIMEOUT, MCP_DOCS_PAGE_URL, MCP_SETTINGS_VIEW } from './mcp.constants';
import WorkflowsTable from './components/tabs/WorkflowsTable.vue';
import MCPConnectWorkflowsModal from './modals/MCPConnectWorkflowsModal.vue';
const i18n = useI18n();
const toast = useToast();
@@ -197,7 +193,7 @@ onMounted(async () => {
<N8nTooltip :content="i18n.baseText('settings.mcp.refresh.tooltip')">
<N8nButton
variant="subtle"
iconOnly
icon-only
data-test-id="mcp-workflows-refresh-button"
size="small"
icon="refresh-cw"
@@ -0,0 +1,24 @@
import { N8nIcon } from '@n8n/design-system';
import { render } from '@testing-library/vue';
/**
* A build-config test, not a feature test. `@n8n/design-system` is consumed from source, so its
* two icon mechanisms are this package's problem, and each package carries its own
* `vite.config.ts`. Both cases fail loudly when a plugin goes missing from that file:
*
* - `box` comes from `~icons/lucide/box`, which only `unplugin-icons` resolves.
* - `webhook` comes from `custom/webhook.svg`, which only `svgLoader` turns into a component.
* Without it the import is a data-URI string, which Vue renders as a tag name.
*
* Keep this test while the module renders any design-system component.
*/
describe('design-system icons', () => {
it.each([
['a lucide icon', 'box'],
['a custom svg icon', 'webhook'],
])('renders %s', (_case, icon) => {
const { container } = render(N8nIcon, { props: { icon } });
expect(container.querySelector('svg')).not.toBeNull();
});
});
@@ -0,0 +1,4 @@
// The shared frontend test harness: the jsdom environment, english messages, and a fresh pinia
// per test. It lives in `@n8n/frontend-test-utils` rather than in `@n8n/vitest-config`, because
// `@n8n/i18n` devDepends on the latter and importing i18n there closes a cycle in the turbo graph.
import '@n8n/frontend-test-utils/setup';

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before

Width:  |  Height:  |  Size: 874 B

After

Width:  |  Height:  |  Size: 874 B

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -2,7 +2,7 @@
import { useI18n } from '@n8n/i18n';
import { useClipboard } from '@n8n/composables/useClipboard';
import { N8nButton, N8nTooltip, N8nInfoTip, N8nInput, N8nLoading } from '@n8n/design-system';
import { MCP_TOOLTIP_DELAY } from '@/features/ai/mcpAccess/mcp.constants';
import { MCP_TOOLTIP_DELAY } from '../mcp.constants';
type Props = {
id: string;
@@ -71,9 +71,9 @@ const handleCopy = async (value: string) => {
placement="bottom"
>
<N8nButton
variant="subtle"
iconOnly
v-if="props.allowCopy && isSupported"
variant="subtle"
icon-only
:icon="copied ? 'check' : 'copy'"
:class="$style['copy-button']"
:disabled="props.valueLoading"
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { LOADING_INDICATOR_TIMEOUT } from '@/features/ai/mcpAccess/mcp.constants';
import { useMCPStore } from '../mcp.store';
import { LOADING_INDICATOR_TIMEOUT } from '../mcp.constants';
import { N8nSelect, N8nOption, N8nText } from '@n8n/design-system';
import { computed, onMounted, ref, useCssModule } from 'vue';
import type { McpAgent } from '@/features/ai/mcpAccess/mcp.types';
import type { McpAgent } from '../mcp.types';
import { useI18n } from '@n8n/i18n';
import { useToast } from '@n8n/composables/useToast';
import { sleep } from '@n8n/utils/sleep';
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed } from 'vue';
import { N8nButton, N8nIcon, N8nTooltip } from '@n8n/design-system';
import { MCP_DOCS_PAGE_URL } from '@/features/ai/mcpAccess/mcp.constants';
import { MCP_DOCS_PAGE_URL } from '../mcp.constants';
import { useI18n } from '@n8n/i18n';
import McpEmptyStateCard from '@/features/ai/mcpAccess/components/McpEmptyStateCard.vue';
import McpEmptyStateCard from './McpEmptyStateCard.vue';
type Props = {
disabled?: boolean;
@@ -3,10 +3,10 @@ import { createTestingPinia } from '@pinia/testing';
import { waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createComponentRenderer, mockedStore, type MockedStore } from '@n8n/frontend-test-utils';
import MCPWorkflowsSelect from '@/features/ai/mcpAccess/components/MCPWorkflowsSelect.vue';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { createWorkflow } from '@/features/ai/mcpAccess/mcp.test.utils';
import type { McpWorkflow } from '@/features/ai/mcpAccess/mcp.types';
import MCPWorkflowsSelect from './MCPWorkflowsSelect.vue';
import { useMCPStore } from '../mcp.store';
import { createWorkflow } from '../mcp.test.utils';
import type { McpWorkflow } from '../mcp.types';
vi.mock('vue-router', async (importOriginal) => ({
...(await importOriginal()),
@@ -1,10 +1,10 @@
<script setup lang="ts">
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { LOADING_INDICATOR_TIMEOUT } from '@/features/ai/mcpAccess/mcp.constants';
import { useMCPStore } from '../mcp.store';
import { LOADING_INDICATOR_TIMEOUT } from '../mcp.constants';
import { N8nSelect, N8nOption } from '@n8n/design-system';
import { computed, onMounted, ref, useCssModule } from 'vue';
import type { McpWorkflow } from '@/features/ai/mcpAccess/mcp.types';
import WorkflowLocation from '@/features/ai/mcpAccess/components/WorkflowLocation.vue';
import type { McpWorkflow } from '../mcp.types';
import WorkflowLocation from './WorkflowLocation.vue';
import { useI18n } from '@n8n/i18n';
import { useToast } from '@n8n/composables/useToast';
@@ -20,7 +20,7 @@ const mockGenerateNewApiKey = vi.fn();
const mockResetCurrentUserMCPKey = vi.fn();
let mockCurrentUserMCPKey: { apiKey: string } | null = { apiKey: 'test-api-key-12345' };
vi.mock('@/features/ai/mcpAccess/mcp.store', () => ({
vi.mock('../mcp.store', () => ({
useMCPStore: () => ({
get currentUserMCPKey() {
return mockCurrentUserMCPKey;
@@ -2,14 +2,11 @@
import { computed, onMounted, ref } from 'vue';
import { useI18n } from '@n8n/i18n';
import { useToast } from '@n8n/composables/useToast';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import {
LOADING_INDICATOR_TIMEOUT,
MCP_TOOLTIP_DELAY,
} from '@/features/ai/mcpAccess/mcp.constants';
import { useMCPStore } from '../mcp.store';
import { LOADING_INDICATOR_TIMEOUT, MCP_TOOLTIP_DELAY } from '../mcp.constants';
import { N8nLoading, N8nTooltip, N8nButton, N8nNotice } from '@n8n/design-system';
import ConnectionParameter from '@/features/ai/mcpAccess/components/ConnectionParameter.vue';
import McpConfigSnippet from '@/features/ai/mcpAccess/components/McpConfigSnippet.vue';
import ConnectionParameter from './ConnectionParameter.vue';
import McpConfigSnippet from './McpConfigSnippet.vue';
type Props = {
serverUrl: string;
@@ -134,7 +131,7 @@ onMounted(async () => {
>
<N8nButton
variant="subtle"
iconOnly
icon-only
icon="refresh-cw"
:disabled="keyRotating"
@click="rotateKey"
@@ -1,7 +1,7 @@
import { within, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createComponentRenderer } from '@n8n/frontend-test-utils';
import McpAllowedCallbackUrlsDialog from '@/features/ai/mcpAccess/components/McpAllowedCallbackUrlsDialog.vue';
import McpAllowedCallbackUrlsDialog from './McpAllowedCallbackUrlsDialog.vue';
const renderComponent = createComponentRenderer(McpAllowedCallbackUrlsDialog);
@@ -12,7 +12,7 @@ import {
N8nRadioGroupItem,
N8nText,
} from '@n8n/design-system';
import { validateRedirectUri } from '@/features/ai/mcpAccess/redirect-uris.utils';
import { validateRedirectUri } from '../redirect-uris.utils';
type UrlMode = 'all' | 'trusted';
@@ -177,7 +177,7 @@ const onSave = () => emit('save', result.value);
v-if="drafts.length > 1 || draft.value.length > 0"
variant="ghost"
size="small"
iconOnly
icon-only
icon="x"
:aria-label="i18n.baseText('generic.delete')"
data-test-id="mcp-callback-url-remove"
@@ -3,7 +3,7 @@ import { computed } from 'vue';
import { useI18n } from '@n8n/i18n';
import { useClipboard } from '@n8n/composables/useClipboard';
import { N8nButton, N8nMarkdown, N8nTooltip } from '@n8n/design-system';
import { MCP_TOOLTIP_DELAY } from '@/features/ai/mcpAccess/mcp.constants';
import { MCP_TOOLTIP_DELAY } from '../mcp.constants';
const props = defineProps<{
/** Raw config snippet; rendered as a copyable code block. */
@@ -42,7 +42,7 @@ const handleCopy = async () => {
<N8nButton
v-if="isSupported && !disabled"
variant="ghost"
iconOnly
icon-only
:icon="copied ? 'check' : 'copy'"
:class="$style['copy-button']"
data-test-id="mcp-config-snippet-copy"
@@ -2,8 +2,8 @@ import { createTestingPinia } from '@pinia/testing';
import { within, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createComponentRenderer, mockedStore, type MockedStore } from '@n8n/frontend-test-utils';
import McpConnectClientDialog from '@/features/ai/mcpAccess/components/McpConnectClientDialog.vue';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import McpConnectClientDialog from './McpConnectClientDialog.vue';
import { useMCPStore } from '../mcp.store';
import { useRootStore } from '@n8n/stores/useRootStore';
let pinia: ReturnType<typeof createTestingPinia>;
@@ -14,12 +14,12 @@ import {
} from '@n8n/design-system';
import type { DropdownMenuItemProps, TabOptions } from '@n8n/design-system';
import { useTelemetry } from '@n8n/composables/useTelemetry';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { getMcpClientCatalog } from '@/features/ai/mcpAccess/mcp.clients.catalog';
import type { McpClientSetup } from '@/features/ai/mcpAccess/mcp.clients.catalog';
import ConnectionParameter from '@/features/ai/mcpAccess/components/ConnectionParameter.vue';
import McpConfigSnippet from '@/features/ai/mcpAccess/components/McpConfigSnippet.vue';
import McpAccessTokenTab from '@/features/ai/mcpAccess/components/McpAccessTokenTab.vue';
import { useMCPStore } from '../mcp.store';
import { getMcpClientCatalog } from '../mcp.clients.catalog';
import type { McpClientSetup } from '../mcp.clients.catalog';
import ConnectionParameter from './ConnectionParameter.vue';
import McpConfigSnippet from './McpConfigSnippet.vue';
import McpAccessTokenTab from './McpAccessTokenTab.vue';
const i18n = useI18n();
const telemetry = useTelemetry();
@@ -226,8 +226,8 @@ const serverUrlDescription = computed(() =>
>
<template #action>
<ConnectionParameter
:class="$style['copy-field']"
id="mcp-install-command"
:class="$style['copy-field']"
:label="''"
:value="activeClient.installCommand ?? ''"
@copy="trackCopy('install-command')"
@@ -257,8 +257,8 @@ const serverUrlDescription = computed(() =>
>
<template #action>
<ConnectionParameter
:class="$style['copy-field']"
id="mcp-auth-command"
:class="$style['copy-field']"
:label="''"
:value="activeClient.authCommand ?? '/mcp'"
@copy="trackCopy('auth-command')"
@@ -300,8 +300,8 @@ const serverUrlDescription = computed(() =>
>
<template #action>
<ConnectionParameter
:class="$style['copy-field']"
id="mcp-web-server-url"
:class="$style['copy-field']"
:label="''"
:value="serverUrl"
@copy="trackCopy('server-url')"
@@ -342,8 +342,8 @@ const serverUrlDescription = computed(() =>
>
<template #action>
<ConnectionParameter
:class="$style['copy-field']"
id="mcp-server-url"
:class="$style['copy-field']"
:label="''"
:value="serverUrl"
@copy="trackCopy('server-url')"
@@ -1,8 +1,8 @@
import userEvent from '@testing-library/user-event';
import { createTestingPinia } from '@pinia/testing';
import { createComponentRenderer } from '@n8n/frontend-test-utils';
import McpConnectedClientRow from '@/features/ai/mcpAccess/components/McpConnectedClientRow.vue';
import { createOAuthClient } from '@/features/ai/mcpAccess/mcp.test.utils';
import McpConnectedClientRow from './McpConnectedClientRow.vue';
import { createOAuthClient } from '../mcp.test.utils';
const createComponent = createComponentRenderer(McpConnectedClientRow, {
pinia: createTestingPinia(),
@@ -7,7 +7,7 @@ import { useI18n } from '@n8n/i18n';
import type { BaseTextKey } from '@n8n/i18n';
import { useRootStore } from '@n8n/stores/useRootStore';
import { getAccessSummary, getClientBrand } from '@/features/ai/mcpAccess/clients.utils';
import { getAccessSummary, getClientBrand } from '../clients.utils';
/**
* One of the current user's connected clients, previewed inline on the MCP
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { N8nText } from '@n8n/design-system';
import McpClientLogoCards from '@/features/ai/mcpAccess/components/McpClientLogoCards.vue';
import McpClientLogoCards from './McpClientLogoCards.vue';
withDefaults(
defineProps<{
@@ -1,7 +1,7 @@
import { within, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createComponentRenderer } from '@n8n/frontend-test-utils';
import McpStatusControl from '@/features/ai/mcpAccess/components/McpStatusControl.vue';
import McpStatusControl from './McpStatusControl.vue';
const renderComponent = createComponentRenderer(McpStatusControl);
@@ -1,6 +1,6 @@
import { createComponentRenderer } from '@n8n/frontend-test-utils';
import WorkflowLocation from '@/features/ai/mcpAccess/components/WorkflowLocation.vue';
import { createHomeProject, createParentFolder } from '@/features/ai/mcpAccess/mcp.test.utils';
import WorkflowLocation from './WorkflowLocation.vue';
import { createHomeProject, createParentFolder } from '../mcp.test.utils';
vi.mock('vue-router', async (importOriginal) => ({
...(await importOriginal()),
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useI18n } from '@n8n/i18n';
import type { McpAgent } from '@/features/ai/mcpAccess/mcp.types';
import type { McpAgent } from '../../mcp.types';
import type { TableHeader, TableOptions, UserAction } from '@n8n/design-system';
import {
N8nActionToggle,
@@ -1,8 +1,8 @@
import userEvent from '@testing-library/user-event';
import { createTestingPinia } from '@pinia/testing';
import { createComponentRenderer } from '@n8n/frontend-test-utils';
import OAuthClientsFilters from '@/features/ai/mcpAccess/components/tabs/OAuthClientsFilters.vue';
import { EMPTY_OAUTH_CLIENT_FILTERS } from '@/features/ai/mcpAccess/clients.utils';
import OAuthClientsFilters from './OAuthClientsFilters.vue';
import { EMPTY_OAUTH_CLIENT_FILTERS } from '../../clients.utils';
const createComponent = createComponentRenderer(OAuthClientsFilters, {
pinia: createTestingPinia(),
@@ -2,8 +2,8 @@ import { waitFor, within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createTestingPinia } from '@pinia/testing';
import { createComponentRenderer } from '@n8n/frontend-test-utils';
import OAuthClientsTable from '@/features/ai/mcpAccess/components/tabs/OAuthClientsTable.vue';
import { createOAuthClient } from '@/features/ai/mcpAccess/mcp.test.utils';
import OAuthClientsTable from './OAuthClientsTable.vue';
import { createOAuthClient } from '../../mcp.test.utils';
const { mockMcpStore, mockHasScope } = vi.hoisted(() => ({
mockMcpStore: {
@@ -23,7 +23,7 @@ const { mockMcpStore, mockHasScope } = vi.hoisted(() => ({
mockHasScope: vi.fn().mockReturnValue(false),
}));
vi.mock('@/features/ai/mcpAccess/mcp.store', () => ({
vi.mock('../../mcp.store', () => ({
useMCPStore: () => mockMcpStore,
}));
@@ -12,16 +12,15 @@ import {
N8nText,
N8nTimeAgo,
} from '@n8n/design-system';
import type { IUser, TabOptions } from '@n8n/design-system';
import type { IUser, TabOptions, TableHeader } from '@n8n/design-system';
import { computed, ref } from 'vue';
import debounce from 'lodash/debounce';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMCPStore } from '../../mcp.store';
import { useRBACStore } from '@n8n/stores/rbac.store';
import { useUsersStore } from '@n8n/stores/users.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import { getDebounceTime } from '@n8n/composables/useDebounce';
import { DEBOUNCE_TIME } from '@n8n/frontend-constants/durations';
import type { TableHeader } from '@n8n/design-system';
import { EMPTY_OAUTH_CLIENT_FILTERS, getAccessSummary, getClientBrand } from '../../clients.utils';
import type { OAuthClientFilters } from '../../clients.utils';
import McpEmptyStateCard from '../McpEmptyStateCard.vue';
@@ -1,8 +1,8 @@
import { waitFor, within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createComponentRenderer, getTooltip } from '@n8n/frontend-test-utils';
import WorkflowsTable from '@/features/ai/mcpAccess/components/tabs/WorkflowsTable.vue';
import { createWorkflow } from '@/features/ai/mcpAccess/mcp.test.utils';
import WorkflowsTable from './WorkflowsTable.vue';
import { createWorkflow } from '../../mcp.test.utils';
vi.mock('vue-router', async (importOriginal) => ({
...(await importOriginal()),
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useI18n } from '@n8n/i18n';
import type { McpWorkflow } from '@/features/ai/mcpAccess/mcp.types';
import type { McpWorkflow } from '../../mcp.types';
import type { TableHeader, TableOptions, UserAction } from '@n8n/design-system';
import {
N8nActionToggle,
@@ -16,8 +16,8 @@ import {
} from '@n8n/design-system';
import { VIEWS } from '@n8n/frontend-constants/views';
import { useRouter } from 'vue-router';
import WorkflowLocation from '@/features/ai/mcpAccess/components/WorkflowLocation.vue';
import { MCP_TOOLTIP_DELAY } from '@/features/ai/mcpAccess/mcp.constants';
import WorkflowLocation from '../WorkflowLocation.vue';
import { MCP_TOOLTIP_DELAY } from '../../mcp.constants';
import { getResourcePermissions } from '@n8n/permissions';
type Props = {
@@ -1,6 +1,6 @@
import { TELEMETRY_EVENT } from '@n8n/telemetry';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useMcp } from './useMcp';
const { trackSpy } = vi.hoisted(() => ({ trackSpy: vi.fn() }));
@@ -4,8 +4,8 @@ import { useI18n } from '@n8n/i18n';
import { useToast } from '@n8n/composables/useToast';
import { useUsersStore } from '@n8n/stores/users.store';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useMcp } from './useMcp';
import { useMCPStore } from '../mcp.store';
/**
* The confirm-then-revoke flow for a connected client, shared by the settings
@@ -0,0 +1,10 @@
// The module's public entry for consumers. The shell imports the descriptor from
// `./mcp.module` instead, so the boot chunk does not pull in the store and the
// components below.
export { useMCPStore } from './mcp.store';
export { useMcp } from './composables/useMcp';
export type { ToggleWorkflowsMcpAccessResponse } from './mcp.api';
export { MCP_DOCS_PAGE_URL, MCP_SCOPE_GROUPS, MCP_SETTINGS_VIEW } from './mcp.constants';
export { getClientBrand } from './clients.utils';
export { default as McpAccessToggle } from './components/McpAccessToggle.vue';
export { default as McpClientLogoCards } from './components/McpClientLogoCards.vue';
@@ -1,4 +1,4 @@
import { getMcpClientCatalog } from '@/features/ai/mcpAccess/mcp.clients.catalog';
import { getMcpClientCatalog } from './mcp.clients.catalog';
const SERVER_URL = 'https://acme.app.n8n.cloud/mcp-server/http';
@@ -8,8 +8,8 @@ import {
MCP_CLIENTS_VIEW,
MCP_SETTINGS_VIEW,
MCP_WORKFLOWS_VIEW,
} from '@/features/ai/mcpAccess/mcp.constants';
import { MCPModule } from '@/features/ai/mcpAccess/module.descriptor';
} from './mcp.constants';
import { MCPModule } from './mcp.module';
describe('MCPModule', () => {
const settingsPage = () => MCPModule.settingsPages?.find((item) => item.id === 'settings-mcp');
@@ -6,15 +6,12 @@ import {
MCP_CLIENTS_VIEW,
MCP_SETTINGS_VIEW,
MCP_WORKFLOWS_VIEW,
} from '@/features/ai/mcpAccess/mcp.constants';
} from './mcp.constants';
const SettingsMCPView = async () => await import('@/features/ai/mcpAccess/SettingsMCPView.vue');
const SettingsMCPWorkflowsView = async () =>
await import('@/features/ai/mcpAccess/SettingsMCPWorkflowsView.vue');
const SettingsMCPAgentsView = async () =>
await import('@/features/ai/mcpAccess/SettingsMCPAgentsView.vue');
const SettingsMCPClientsView = async () =>
await import('@/features/ai/mcpAccess/SettingsMCPClientsView.vue');
const SettingsMCPView = async () => await import('./SettingsMCPView.vue');
const SettingsMCPWorkflowsView = async () => await import('./SettingsMCPWorkflowsView.vue');
const SettingsMCPAgentsView = async () => await import('./SettingsMCPAgentsView.vue');
const SettingsMCPClientsView = async () => await import('./SettingsMCPClientsView.vue');
export const MCPModule = defineFrontendModule({
id: 'mcp',
@@ -21,14 +21,11 @@ import {
type ToggleWorkflowsMcpAccessTarget,
type ToggleAgentsMcpAccessResponse,
type ToggleAgentsMcpAccessTarget,
} from '@/features/ai/mcpAccess/mcp.api';
import type { McpAgent, McpWorkflow } from '@/features/ai/mcpAccess/mcp.types';
} from './mcp.api';
import type { McpAgent, McpWorkflow } from './mcp.types';
import { computed, ref } from 'vue';
import { useSettingsStore } from '@n8n/stores/settings.store';
import {
EMPTY_OAUTH_CLIENT_FILTERS,
type OAuthClientFilters,
} from '@/features/ai/mcpAccess/clients.utils';
import { EMPTY_OAUTH_CLIENT_FILTERS, type OAuthClientFilters } from './clients.utils';
import type {
ApiKey,
InstanceMcpClientStatsResponseDto,

Some files were not shown because too many files have changed in this diff Show More