feat(editor): Allow MCP access only for webhook triggered workflows (no-changelog) (#20414)

This commit is contained in:
Milorad FIlipović 2025-10-07 15:20:45 +02:00 committed by GitHub
parent c22f86224a
commit 278ca8b9e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 372 additions and 34 deletions

View File

@ -1,21 +1,77 @@
import { Logger, ModuleRegistry } from '@n8n/backend-common';
import { type ApiKey, type AuthenticatedRequest } from '@n8n/db';
import { type ApiKey, type AuthenticatedRequest, WorkflowEntity, User, Role } from '@n8n/db';
import { Container } from '@n8n/di';
import type { Response } from 'express';
import { mock, mockDeep } from 'jest-mock-extended';
import { HTTP_REQUEST_NODE_TYPE, WEBHOOK_NODE_TYPE, type INode } from 'n8n-workflow';
import { UpdateMcpSettingsDto } from '../dto/update-mcp-settings.dto';
import { McpServerApiKeyService } from '../mcp-api-key.service';
import { McpSettingsController } from '../mcp.settings.controller';
import { McpSettingsService } from '../mcp.settings.service';
const createReq = (body: unknown): AuthenticatedRequest =>
({ body }) as unknown as AuthenticatedRequest;
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { WorkflowFinderService } from '@/workflows/workflow-finder.service';
import { WorkflowService } from '@/workflows/workflow.service';
const createReq = (
body: unknown,
overrides: Partial<AuthenticatedRequest> = {},
): AuthenticatedRequest => ({ body, ...overrides }) as unknown as AuthenticatedRequest;
const createRole = () =>
Object.assign(new Role(), {
slug: 'member',
displayName: 'Member',
description: null,
systemRole: false,
roleType: 'global' as const,
projectRelations: [],
scopes: [],
});
const createUser = (overrides: Partial<User> = {}) =>
Object.assign(
new User(),
{
id: 'user-1',
email: 'user@example.com',
firstName: 'Test',
lastName: 'User',
password: null,
personalizationAnswers: null,
settings: null,
role: createRole(),
authIdentities: [],
apiKeys: [],
sharedWorkflows: [],
sharedCredentials: [],
projectRelations: [],
disabled: false,
mfaEnabled: false,
mfaSecret: null,
mfaRecoveryCodes: [],
lastActiveAt: null,
isPending: false,
},
overrides,
);
const createRes = () => {
const res = mock<Response>();
res.status.mockReturnThis();
res.json.mockReturnThis();
return res;
};
describe('McpSettingsController', () => {
const logger = mock<Logger>();
const moduleRegistry = mockDeep<ModuleRegistry>();
const mcpSettingsService = mock<McpSettingsService>();
const mcpServerApiKeyService = mockDeep<McpServerApiKeyService>();
const workflowFinderService = mock<WorkflowFinderService>();
const workflowService = mock<WorkflowService>();
let controller: McpSettingsController;
@ -25,9 +81,15 @@ describe('McpSettingsController', () => {
Container.set(McpSettingsService, mcpSettingsService);
Container.set(ModuleRegistry, moduleRegistry);
Container.set(McpServerApiKeyService, mcpServerApiKeyService);
Container.set(WorkflowFinderService, workflowFinderService);
Container.set(WorkflowService, workflowService);
controller = Container.get(McpSettingsController);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('updateSettings', () => {
test('disables MCP access correctly', async () => {
const req = createReq({ mcpAccessEnabled: false });
@ -35,7 +97,7 @@ describe('McpSettingsController', () => {
mcpSettingsService.setEnabled.mockResolvedValue(undefined);
moduleRegistry.refreshModuleSettings.mockResolvedValue(null);
const res = new Response();
const res = createRes();
const result = await controller.updateSettings(req, res, dto);
expect(mcpSettingsService.setEnabled).toHaveBeenCalledWith(false);
@ -49,7 +111,7 @@ describe('McpSettingsController', () => {
mcpSettingsService.setEnabled.mockResolvedValue(undefined);
moduleRegistry.refreshModuleSettings.mockResolvedValue(null);
const res = new Response();
const res = createRes();
const result = await controller.updateSettings(req, res, dto);
expect(mcpSettingsService.setEnabled).toHaveBeenCalledWith(true);
@ -65,7 +127,7 @@ describe('McpSettingsController', () => {
mcpSettingsService.setEnabled.mockResolvedValue(undefined);
moduleRegistry.refreshModuleSettings.mockRejectedValue(error);
const res = new Response();
const res = createRes();
const result = await controller.updateSettings(req, res, dto);
expect(mcpSettingsService.setEnabled).toHaveBeenCalledWith(true);
@ -83,7 +145,7 @@ describe('McpSettingsController', () => {
});
describe('getApiKeyForMcpServer', () => {
const mockUser = { id: 'user123', role: { slug: 'member' } };
const mockUser = createUser({ id: 'user123', email: 'user123@example.com' });
const mockApiKey = {
id: 'api-key-123',
key: 'mcp-key-abc123',
@ -92,7 +154,7 @@ describe('McpSettingsController', () => {
} as unknown as ApiKey;
test('returns API key from getOrCreateApiKey', async () => {
const req = { user: mockUser } as AuthenticatedRequest;
const req = createReq({}, { user: mockUser });
mcpServerApiKeyService.getOrCreateApiKey.mockResolvedValue(mockApiKey);
const result = await controller.getApiKeyForMcpServer(req);
@ -103,7 +165,7 @@ describe('McpSettingsController', () => {
});
describe('rotateApiKeyForMcpServer', () => {
const mockUser = { id: 'user123', role: { slug: 'member' } };
const mockUser = createUser({ id: 'user123', email: 'user123@example.com' });
const mockApiKey = {
id: 'api-key-123',
key: 'mcp-key-abc123',
@ -112,7 +174,7 @@ describe('McpSettingsController', () => {
} as unknown as ApiKey;
test('successfully rotates API key', async () => {
const req = { user: mockUser } as AuthenticatedRequest;
const req = createReq({}, { user: mockUser });
mcpServerApiKeyService.rotateMcpServerApiKey.mockResolvedValue(mockApiKey);
@ -122,4 +184,138 @@ describe('McpSettingsController', () => {
expect(result).toEqual(mockApiKey);
});
});
describe('toggleWorkflowMCPAccess', () => {
const user = createUser();
const workflowId = 'workflow-1';
const createWebhookNode = (overrides: Partial<INode> = {}): INode => ({
id: 'node-1',
name: 'Webhook',
type: WEBHOOK_NODE_TYPE,
typeVersion: 1,
position: [0, 0],
parameters: {},
...overrides,
});
const createWorkflow = (overrides: Partial<WorkflowEntity> = {}) => {
const entity = new WorkflowEntity();
entity.id = workflowId;
entity.active = true;
entity.nodes = [createWebhookNode()];
entity.settings = { saveManualExecutions: true };
entity.versionId = 'current-version-id';
return Object.assign(entity, overrides);
};
test('throws when workflow cannot be accessed', async () => {
workflowFinderService.findWorkflowForUser.mockResolvedValue(null);
const req = createReq({}, { user });
await expect(
controller.toggleWorkflowMCPAccess(req, mock<Response>(), workflowId, {
availableInMCP: true,
}),
).rejects.toThrow(
new NotFoundError(
'Could not load the workflow - you can only access workflows available to you',
),
);
expect(workflowService.update).not.toHaveBeenCalled();
});
test('rejects enabling MCP for inactive workflows', async () => {
workflowFinderService.findWorkflowForUser.mockResolvedValue(
createWorkflow({ active: false }),
);
await expect(
controller.toggleWorkflowMCPAccess(createReq({}, { user }), mock<Response>(), workflowId, {
availableInMCP: true,
}),
).rejects.toThrow(new BadRequestError('MCP access can only be set for active workflows'));
expect(workflowService.update).not.toHaveBeenCalled();
});
test('rejects enabling MCP without active webhook nodes', async () => {
workflowFinderService.findWorkflowForUser.mockResolvedValue(
createWorkflow({
nodes: [
createWebhookNode({ disabled: true }),
{
id: 'node-2',
name: 'HTTP Request',
type: HTTP_REQUEST_NODE_TYPE,
typeVersion: 1,
position: [10, 10],
parameters: {},
},
],
}),
);
await expect(
controller.toggleWorkflowMCPAccess(createReq({}, { user }), mock<Response>(), workflowId, {
availableInMCP: true,
}),
).rejects.toThrow(
new BadRequestError('MCP access can only be set for webhook-triggered workflows'),
);
expect(workflowService.update).not.toHaveBeenCalled();
});
test('persists MCP availability when validation passes', async () => {
const workflow = createWorkflow();
workflowFinderService.findWorkflowForUser.mockResolvedValue(workflow);
workflowService.update.mockResolvedValue({
id: workflowId,
settings: { saveManualExecutions: true, availableInMCP: true },
versionId: 'updated-version-id',
} as unknown as WorkflowEntity);
const req = createReq({}, { user });
const response = await controller.toggleWorkflowMCPAccess(req, mock<Response>(), workflowId, {
availableInMCP: true,
});
expect(workflowService.update).toHaveBeenCalledTimes(1);
const updateArgs = workflowService.update.mock.calls[0];
expect(updateArgs[0]).toEqual(user);
expect(updateArgs[1]).toBeInstanceOf(WorkflowEntity);
expect(updateArgs[1].settings).toEqual({ saveManualExecutions: true, availableInMCP: true });
expect(updateArgs[1].versionId).toEqual('current-version-id');
expect(updateArgs[2]).toEqual(workflowId);
expect(updateArgs[5]).toEqual(false);
expect(response).toEqual({
id: workflowId,
settings: { saveManualExecutions: true, availableInMCP: true },
versionId: 'updated-version-id',
});
});
test('rejects disabling MCP for inactive workflows', async () => {
workflowFinderService.findWorkflowForUser.mockResolvedValue(
createWorkflow({ active: false }),
);
workflowService.update.mockResolvedValue({
id: workflowId,
settings: { saveManualExecutions: true, availableInMCP: false },
versionId: 'client-version',
} as unknown as WorkflowEntity);
const req = createReq({}, { user });
await expect(
controller.toggleWorkflowMCPAccess(req, mock<Response>(), workflowId, {
availableInMCP: false,
}),
).rejects.toThrow(new BadRequestError('MCP access can only be set for active workflows'));
expect(workflowService.update).not.toHaveBeenCalled();
});
});
});

View File

@ -1,4 +1,5 @@
import type { WorkflowEntity } from '@n8n/db';
import { MANUAL_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE } from 'n8n-workflow';
export const createWorkflow = (overrides: Partial<WorkflowEntity> = {}) => ({
id: 'wf-1',
@ -7,7 +8,7 @@ export const createWorkflow = (overrides: Partial<WorkflowEntity> = {}) => ({
{
id: 'node-1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
type: WEBHOOK_NODE_TYPE,
typeVersion: 1,
position: [0, 0],
disabled: false,
@ -17,7 +18,7 @@ export const createWorkflow = (overrides: Partial<WorkflowEntity> = {}) => ({
{
id: 'node-2',
name: 'Start',
type: 'n8n-nodes-base.start',
type: MANUAL_TRIGGER_NODE_TYPE,
typeVersion: 1,
position: [100, 0],
disabled: false,

View File

@ -6,6 +6,7 @@ import { createWorkflow } from './mock.utils';
import { searchWorkflows, createSearchWorkflowsTool } from '../tools/search-workflows.tool';
import { WorkflowService } from '@/workflows/workflow.service';
import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from 'n8n-workflow';
describe('search-workflows MCP tool', () => {
const user = Object.assign(new User(), { id: 'user-1' });
@ -16,7 +17,7 @@ describe('search-workflows MCP tool', () => {
createWorkflow({
id: 'wrap-1',
name: 'Wrapper',
nodes: [{ name: 'Start', type: 'n8n-nodes-base.start' } as INode],
nodes: [{ name: 'Start', type: MANUAL_TRIGGER_NODE_TYPE } as INode],
}),
];
@ -40,13 +41,15 @@ describe('search-workflows MCP tool', () => {
createWorkflow({
id: 'a',
name: 'Alpha',
nodes: [{ name: 'Start', type: 'n8n-nodes-base.start' } as INode],
nodes: [{ name: 'Start', type: MANUAL_TRIGGER_NODE_TYPE } as INode],
}),
createWorkflow({
id: 'b',
name: 'Beta',
active: true,
nodes: [{ name: 'Cron', type: 'n8n-nodes-base.cron' } as INode],
nodes: [
{ name: 'Execute subworkflow', type: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE } as INode,
],
}),
];
@ -64,7 +67,7 @@ describe('search-workflows MCP tool', () => {
createdAt: new Date('2024-01-01T00:00:00.000Z').toISOString(),
updatedAt: new Date('2024-01-02T00:00:00.000Z').toISOString(),
triggerCount: 1,
nodes: [{ name: 'Start', type: 'n8n-nodes-base.start' }],
nodes: [{ name: 'Start', type: MANUAL_TRIGGER_NODE_TYPE }],
},
{
id: 'b',
@ -73,7 +76,7 @@ describe('search-workflows MCP tool', () => {
createdAt: new Date('2024-01-01T00:00:00.000Z').toISOString(),
updatedAt: new Date('2024-01-02T00:00:00.000Z').toISOString(),
triggerCount: 1,
nodes: [{ name: 'Cron', type: 'n8n-nodes-base.cron' }],
nodes: [{ name: 'Execute subworkflow', type: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE }],
},
]);
});

View File

@ -7,6 +7,7 @@ import type {
INodeParameters,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
import { WEBHOOK_NODE_TYPE } from 'n8n-workflow';
import { buildWebhookPath, getWebhookDetails } from '../tools/webhook-utils';
@ -40,7 +41,7 @@ const createWebhookNode = (
const base: INode = {
id: '1',
name: 'Webhook',
type: 'n8n-nodes-base.webhook',
type: WEBHOOK_NODE_TYPE,
typeVersion: 1,
position: [0, 0],
parameters: {},

View File

@ -0,0 +1,6 @@
import { z } from 'zod';
import { Z } from 'zod-class';
export class UpdateWorkflowAvailabilityDto extends Z.class({
availableInMCP: z.boolean(),
}) {}

View File

@ -1,11 +1,19 @@
import { ModuleRegistry, Logger } from '@n8n/backend-common';
import { type AuthenticatedRequest } from '@n8n/db';
import { Body, Post, Get, Patch, RestController, GlobalScope } from '@n8n/decorators';
import { type AuthenticatedRequest, WorkflowEntity } from '@n8n/db';
import { Body, Post, Get, Patch, RestController, GlobalScope, Param } from '@n8n/decorators';
import type { Response } from 'express';
import { WEBHOOK_NODE_TYPE } from 'n8n-workflow';
import { UpdateMcpSettingsDto } from './dto/update-mcp-settings.dto';
import { UpdateWorkflowAvailabilityDto } from './dto/update-workflow-availability.dto';
import { McpServerApiKeyService } from './mcp-api-key.service';
import { McpSettingsService } from './mcp.settings.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { WorkflowFinderService } from '@/workflows/workflow-finder.service';
import { WorkflowService } from '@/workflows/workflow.service';
@RestController('/mcp')
export class McpSettingsController {
constructor(
@ -13,6 +21,8 @@ export class McpSettingsController {
private readonly logger: Logger,
private readonly moduleRegistry: ModuleRegistry,
private readonly mcpServerApiKeyService: McpServerApiKeyService,
private readonly workflowFinderService: WorkflowFinderService,
private readonly workflowService: WorkflowService,
) {}
@GlobalScope('mcp:manage')
@ -45,4 +55,62 @@ export class McpSettingsController {
async rotateApiKeyForMcpServer(req: AuthenticatedRequest) {
return await this.mcpServerApiKeyService.rotateMcpServerApiKey(req.user);
}
@GlobalScope('mcp:manage')
@Patch('/workflows/:workflowId/toggle-access')
async toggleWorkflowMCPAccess(
req: AuthenticatedRequest,
_res: Response,
@Param('workflowId') workflowId: string,
@Body dto: UpdateWorkflowAvailabilityDto,
) {
const workflow = await this.workflowFinderService.findWorkflowForUser(workflowId, req.user, [
'workflow:update',
]);
if (!workflow) {
this.logger.warn('User attempted to update MCP availability without permissions', {
workflowId,
userId: req.user.id,
});
throw new NotFoundError(
'Could not load the workflow - you can only access workflows available to you',
);
}
if (!workflow.active) {
throw new BadRequestError('MCP access can only be set for active workflows');
}
const hasWebhooks = workflow.nodes.some(
(node) => node.type === WEBHOOK_NODE_TYPE && node.disabled !== true,
);
if (!hasWebhooks) {
throw new BadRequestError('MCP access can only be set for webhook-triggered workflows');
}
const workflowUpdate = new WorkflowEntity();
const currentSettings = workflow.settings ?? {};
workflowUpdate.settings = {
...currentSettings,
availableInMCP: dto.availableInMCP,
};
workflowUpdate.versionId = workflow.versionId;
const updatedWorkflow = await this.workflowService.update(
req.user,
workflowUpdate,
workflowId,
undefined, // tags
undefined, // parentFolderId
false, // forceSave
);
return {
id: updatedWorkflow.id,
settings: updatedWorkflow.settings,
versionId: updatedWorkflow.versionId,
};
}
}

View File

@ -1,5 +1,5 @@
import type { User } from '@n8n/db';
import { UserError } from 'n8n-workflow';
import { UserError, WEBHOOK_NODE_TYPE } from 'n8n-workflow';
import z from 'zod';
import type { ToolDefinition, WorkflowDetailsResult } from '../mcp.types';
@ -68,7 +68,7 @@ export async function getWorkflowDetails(
}
const webhooks = workflow.nodes.filter(
(node) => node.type === 'n8n-nodes-base.webhook' && node.disabled !== true,
(node) => node.type === WEBHOOK_NODE_TYPE && node.disabled !== true,
);
let triggerNotice = await getWebhookDetails(

View File

@ -1146,6 +1146,7 @@
"mainSidebar.workersView": "Workers",
"mainSidebar.whatsNew": "Whats New",
"mainSidebar.whatsNew.fullChangelog": "Full changelog",
"mcp.workflowNotEligable.description": "Only active, webhook-triggered workflows can be accessible through MCP",
"menuActions.duplicate": "Duplicate",
"menuActions.download": "Download",
"menuActions.push": "Push to Git",

View File

@ -23,7 +23,7 @@ import { useRoute, useRouter } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry';
import { ResourceType } from '@/utils/projects.utils';
import type { EventBus } from '@n8n/utils/event-bus';
import type { WorkflowResource } from '@/Interface';
import type { UserAction, WorkflowResource } from '@/Interface';
import type { IUser } from 'n8n-workflow';
import { type ProjectSharingData, ProjectTypes } from '@/types/projects.types';
import type { PathItem } from '@n8n/design-system/components/N8nBreadcrumbs/Breadcrumbs.vue';
@ -39,6 +39,7 @@ import {
N8nText,
N8nTooltip,
} from '@n8n/design-system';
import { useMCPStore } from '@/features/mcpAccess/mcp.store';
const WORKFLOW_LIST_ITEM_ACTIONS = {
OPEN: 'open',
SHARE: 'share',
@ -101,6 +102,7 @@ const usersStore = useUsersStore();
const workflowsStore = useWorkflowsStore();
const projectsStore = useProjectsStore();
const foldersStore = useFoldersStore();
const mcpStore = useMCPStore();
const hiddenBreadcrumbsItemsAsync = ref<Promise<PathItem[]>>(new Promise(() => {}));
const cachedHiddenBreadcrumbsItems = ref<PathItem[]>([]);
@ -150,7 +152,7 @@ const cardBreadcrumbs = computed<PathItem[]>(() => {
});
const actions = computed(() => {
const items = [
const items: Array<UserAction<IUser>> = [
{
label: locale.baseText('workflows.item.open'),
value: WORKFLOW_LIST_ITEM_ACTIONS.OPEN,
@ -210,11 +212,13 @@ const actions = computed(() => {
items.push({
label: locale.baseText('workflows.item.disableMCPAccess'),
value: WORKFLOW_LIST_ITEM_ACTIONS.REMOVE_MCP_ACCESS,
disabled: !props.data.active,
});
} else {
items.push({
label: locale.baseText('workflows.item.enableMCPAccess'),
value: WORKFLOW_LIST_ITEM_ACTIONS.ENABLE_MCP_ACCESS,
disabled: !props.data.active,
});
}
}
@ -331,7 +335,7 @@ async function onAction(action: string) {
async function toggleMCPAccess(enabled: boolean) {
try {
await workflowsStore.updateWorkflowSetting(props.data.id, 'availableInMCP', enabled);
await mcpStore.toggleWorkflowMcpAccess(props.data.id, enabled);
mcpToggleStatus.value = enabled;
} catch (error) {
toast.showError(error, locale.baseText('workflowSettings.toggleMCP.error.title'));

View File

@ -8,6 +8,7 @@ import Modal from '@/components/Modal.vue';
import {
EnterpriseEditionFeature,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
WEBHOOK_NODE_TYPE,
WORKFLOW_SETTINGS_MODAL_KEY,
} from '@/constants';
import type { WorkflowSettings } from 'n8n-workflow';
@ -97,6 +98,16 @@ const workflowOwnerName = computed(() => {
});
const workflowPermissions = computed(() => getResourcePermissions(workflow.value?.scopes).workflow);
const isEligibleForMCPAccess = computed(() => {
if (!workflow.value?.active) {
return false;
}
// If it's active, check if workflow has at least one enabled webhook trigger:
return workflow.value?.nodes.some(
(node) => node.type === WEBHOOK_NODE_TYPE && node.disabled !== true,
);
});
const onCallerIdsInput = (str: string) => {
workflowSettings.value.callerIds = /^[a-zA-Z0-9,\s]+$/.test(str)
? str
@ -846,7 +857,11 @@ onBeforeUnmount(() => {
{{ i18n.baseText('workflowSettings.availableInMCP') }}
<N8nTooltip placement="top">
<template #content>
{{ i18n.baseText('workflowSettings.availableInMCP.tooltip') }}
{{
isEligibleForMCPAccess
? i18n.baseText('workflowSettings.availableInMCP.tooltip')
: i18n.baseText('mcp.workflowNotEligable.description')
}}
</template>
<N8nIcon icon="circle-help" />
</N8nTooltip>
@ -854,13 +869,18 @@ onBeforeUnmount(() => {
</ElCol>
<ElCol :span="14">
<div>
<ElSwitch
ref="inputField"
:disabled="readOnlyEnv || !workflowPermissions.update"
:model-value="workflowSettings.availableInMCP ?? false"
data-test-id="workflow-settings-available-in-mcp"
@update:model-value="toggleAvailableInMCP"
></ElSwitch>
<N8nTooltip placement="top" :disabled="isEligibleForMCPAccess">
<template #content>
{{ i18n.baseText('mcp.workflowNotEligable.description') }}
</template>
<ElSwitch
ref="inputField"
:disabled="readOnlyEnv || !workflowPermissions.update || !isEligibleForMCPAccess"
:model-value="workflowSettings.availableInMCP ?? false"
data-test-id="workflow-settings-available-in-mcp"
@update:model-value="toggleAvailableInMCP"
></ElSwitch>
</N8nTooltip>
</div>
</ElCol>
</ElRow>

View File

@ -1,4 +1,5 @@
import type { ApiKey } from '@n8n/api-types';
import type { IWorkflowSettings } from '@/Interface';
import type { IRestApiContext } from '@n8n/rest-api-client';
import { makeRestApiRequest } from '@n8n/rest-api-client';
@ -26,3 +27,18 @@ export async function fetchApiKey(context: IRestApiContext): Promise<ApiKey> {
export async function rotateApiKey(context: IRestApiContext): Promise<ApiKey> {
return await makeRestApiRequest(context, 'POST', '/mcp/api-key/rotate');
}
export async function toggleWorkflowMcpAccessApi(
context: IRestApiContext,
workflowId: string,
availableInMCP: boolean,
): Promise<{ id: string; settings: IWorkflowSettings | undefined; versionId: string }> {
return await makeRestApiRequest(
context,
'PATCH',
`/mcp/workflows/${encodeURIComponent(workflowId)}/toggle-access`,
{
availableInMCP,
},
);
}

View File

@ -3,7 +3,12 @@ import { MCP_STORE } from './mcp.constants';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { WorkflowListItem } from '@/Interface';
import { useRootStore } from '@n8n/stores/useRootStore';
import { fetchApiKey, updateMcpSettings, rotateApiKey } from '@/features/mcpAccess/mcp.api';
import {
updateMcpSettings,
toggleWorkflowMcpAccessApi,
fetchApiKey,
rotateApiKey,
} from '@/features/mcpAccess/mcp.api';
import { computed, ref } from 'vue';
import { useSettingsStore } from '@/stores/settings.store';
import { isWorkflowListItem } from '@/utils/typeGuards';
@ -46,6 +51,22 @@ export const useMCPStore = defineStore(MCP_STORE, () => {
return updated;
}
async function toggleWorkflowMcpAccess(
workflowId: string,
availableInMCP: boolean,
): Promise<{
id: string;
settings: { availableInMCP?: boolean } | undefined;
versionId: string;
}> {
const response = await toggleWorkflowMcpAccessApi(
rootStore.restApiContext,
workflowId,
availableInMCP,
);
return response;
}
async function getOrCreateApiKey(): Promise<ApiKey> {
const apiKey = await fetchApiKey(rootStore.restApiContext);
currentUserMCPKey.value = apiKey;
@ -62,6 +83,7 @@ export const useMCPStore = defineStore(MCP_STORE, () => {
mcpAccessEnabled,
fetchWorkflowsAvailableForMCP,
setMcpAccessEnabled,
toggleWorkflowMcpAccess,
currentUserMCPKey,
getOrCreateApiKey,
generateNewApiKey,