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>
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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: 469 B After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 456 B After Width: | Height: | Size: 456 B |
|
Before Width: | Height: | Size: 588 B After Width: | Height: | Size: 588 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,
|
||||