Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
5c6f5db
feat(editor): Connect workflows from MCP settings page
MiloradFilipovic Dec 9, 2025
af66e29
✨ Fetch eligible workflows in the modal
MiloradFilipovic Dec 9, 2025
e204832
⚡ Only list workflows that don't have access already
MiloradFilipovic Dec 9, 2025
565e46b
✨ Extract workflows search select
MiloradFilipovic Dec 9, 2025
a6b771e
⚡ Fix back-end query
MiloradFilipovic Dec 9, 2025
12966fc
⚡ Toggle workflow MCP access when saved
MiloradFilipovic Dec 9, 2025
c93f4d3
✨ Implement search
MiloradFilipovic Dec 9, 2025
ee47870
✨ Allow specifying scope when listing workflows
MiloradFilipovic Dec 9, 2025
39fe9d2
🔨 Extract workflow location and re-use it in table and select
MiloradFilipovic Dec 9, 2025
7b21a1c
👌 Update notice, reduce loading timeout
MiloradFilipovic Dec 10, 2025
dfc12f9
👌 Adjust loading state style
MiloradFilipovic Dec 10, 2025
9a119d6
👌 Fix responsiveness of workflow table
MiloradFilipovic Dec 10, 2025
551614b
📈 Add telemetry
MiloradFilipovic Dec 10, 2025
f9d184d
⚡ Handle modal dismiss on ESC or click outside
MiloradFilipovic Dec 10, 2025
07e2287
👌 Update notice copy
MiloradFilipovic Dec 10, 2025
6fd22a7
⚡ Handle errors, add missing props
MiloradFilipovic Dec 10, 2025
20e5092
👕 Update ref type
MiloradFilipovic Dec 10, 2025
b54da23
⚡ Handle empty state better
MiloradFilipovic Dec 10, 2025
383a9b3
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 11, 2025
355ba17
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 11, 2025
ed0ea29
🔥 Remove freaking build log
MiloradFilipovic Dec 11, 2025
a8c9c10
⏪ Revert temporary backend filtering logic
MiloradFilipovic Dec 11, 2025
3981c1b
⚡ Update middleware to work with mcp endpoints
MiloradFilipovic Dec 11, 2025
becde45
✨ Update trigger filter to support arrays
MiloradFilipovic Dec 11, 2025
f0174d9
👌 Update allowedTags in the notice
MiloradFilipovic Dec 11, 2025
f4edaae
✅ Update `mcp.settings.controller` tests
MiloradFilipovic Dec 11, 2025
7641da9
✔️ Add `workflow.service` tests for scope parameter
MiloradFilipovic Dec 11, 2025
fcfd5e0
✅ Update workflows table tests
MiloradFilipovic Dec 11, 2025
efa03fa
✅ Add more tests for the main mcp settings page
MiloradFilipovic Dec 11, 2025
d4d36a9
🔥 Remove unused emit
MiloradFilipovic Dec 11, 2025
044cf4a
✅ Add tests for `WorkflowLocation` component
MiloradFilipovic Dec 11, 2025
baf1f42
✔️ Fix tests
MiloradFilipovic Dec 11, 2025
b003370
✅ Add workflow select tests
MiloradFilipovic Dec 11, 2025
84b246c
🔨 Extracting reusable methods to test utils
MiloradFilipovic Dec 11, 2025
7452720
🔨 Extract more methods
MiloradFilipovic Dec 11, 2025
dc554c1
✅ Adding tests for the connection modal
MiloradFilipovic Dec 11, 2025
f19b8fc
✔️ Update notice tests
MiloradFilipovic Dec 11, 2025
e4eb6f3
🔥 Remove dead css class
MiloradFilipovic Dec 11, 2025
f37164b
👌 Update timeout handling in workflows select
MiloradFilipovic Dec 11, 2025
82986c8
👌 Update modal title
MiloradFilipovic Dec 11, 2025
c3d4125
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 11, 2025
aa26661
👕 Fix async function
MiloradFilipovic Dec 11, 2025
fe6964f
⚡ Wait for workflow select to be ready before focusing
MiloradFilipovic Dec 11, 2025
cec1e99
👕 Fix async method
MiloradFilipovic Dec 11, 2025
29e7dc2
⚡ Count `null` as `false` in mcp access filter
MiloradFilipovic Dec 11, 2025
3f65398
👌 Use `prefix` slot for search icon
MiloradFilipovic Dec 11, 2025
4207453
👌 Remove workflows table empty state description
MiloradFilipovic Dec 11, 2025
1359753
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 11, 2025
dce37e6
✔️ Update workflows table tests
MiloradFilipovic Dec 11, 2025
36114d9
Merge remote-tracking branch 'origin/ADO-4437-connect-workflows-from-…
MiloradFilipovic Dec 11, 2025
be78239
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 12, 2025
59c5b3a
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 12, 2025
f78bce3
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 12, 2025
f5f68bb
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 12, 2025
b148ec7
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 12, 2025
902eac9
📈 Added simple logging for MCP requests
MiloradFilipovic Dec 12, 2025
a493283
Merge remote-tracking branch 'origin/ADO-4437-connect-workflows-from-…
MiloradFilipovic Dec 12, 2025
d7075d9
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 12, 2025
791ba6a
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 15, 2025
c88867d
💄 Make description link inline
MiloradFilipovic Dec 15, 2025
71f1edc
Merge branch 'master' into ADO-4437-connect-workflows-from-mcp-page
MiloradFilipovic Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
961 changes: 961 additions & 0 deletions build.log

Large diffs are not rendered by default.

75 changes: 63 additions & 12 deletions packages/@n8n/db/src/repositories/workflow.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
this.applyProjectFilter(qb, filter);
this.applyParentFolderFilter(qb, filter);
this.applyNodeTypesFilter(qb, filter);
this.applyActiveVersionNodeTypesFilter(qb, filter);
this.applyAvailableInMCPFilter(qb, filter);
}

Expand All @@ -483,18 +484,39 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
if (typeof filter?.availableInMCP === 'boolean') {
const dbType = this.globalConfig.database.type;

if (['postgresdb'].includes(dbType)) {
qb.andWhere("workflow.settings ->> 'availableInMCP' = :availableInMCP", {
availableInMCP: filter.availableInMCP.toString(),
});
} else if (['mysqldb', 'mariadb'].includes(dbType)) {
qb.andWhere("JSON_EXTRACT(workflow.settings, '$.availableInMCP') = :availableInMCP", {
availableInMCP: filter.availableInMCP,
});
} else if (dbType === 'sqlite') {
qb.andWhere("JSON_EXTRACT(workflow.settings, '$.availableInMCP') = :availableInMCP", {
availableInMCP: filter.availableInMCP ? 1 : 0, // SQLite stores booleans as 0/1
});
if (filter.availableInMCP) {
// Filter for workflows where availableInMCP is explicitly true
if (['postgresdb'].includes(dbType)) {
qb.andWhere("workflow.settings ->> 'availableInMCP' = :availableInMCP", {
availableInMCP: 'true',
});
} else if (['mysqldb', 'mariadb'].includes(dbType)) {
qb.andWhere("JSON_EXTRACT(workflow.settings, '$.availableInMCP') = :availableInMCP", {
availableInMCP: true,
});
} else if (dbType === 'sqlite') {
qb.andWhere("JSON_EXTRACT(workflow.settings, '$.availableInMCP') = :availableInMCP", {
availableInMCP: 1,
});
}
} else {
// Filter for workflows where availableInMCP is not true (false, null, or missing)
if (['postgresdb'].includes(dbType)) {
qb.andWhere(
"(workflow.settings ->> 'availableInMCP' IS NULL OR workflow.settings ->> 'availableInMCP' != :availableInMCP)",
{ availableInMCP: 'true' },
);
} else if (['mysqldb', 'mariadb'].includes(dbType)) {
qb.andWhere(
"(JSON_EXTRACT(workflow.settings, '$.availableInMCP') IS NULL OR JSON_EXTRACT(workflow.settings, '$.availableInMCP') != :availableInMCP)",
{ availableInMCP: true },
);
} else if (dbType === 'sqlite') {
qb.andWhere(
"(JSON_EXTRACT(workflow.settings, '$.availableInMCP') IS NULL OR JSON_EXTRACT(workflow.settings, '$.availableInMCP') != :availableInMCP)",
{ availableInMCP: 1 },
);
}
}
}
}
Expand Down Expand Up @@ -652,6 +674,35 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
qb.andWhere(whereClause, parameters);
}

/**
* Filter workflows by node types in the published (active) version.
* This joins the workflow_history table to check nodes in the activeVersion.
*/
private applyActiveVersionNodeTypesFilter(
qb: SelectQueryBuilder<WorkflowEntity>,
filter: ListQuery.Options['filter'],
): void {
const nodeTypes = isStringArray(filter?.activeVersionNodeTypes)
? filter.activeVersionNodeTypes
: [];

if (!nodeTypes.length) return;

// Join the activeVersion relation if not already joined
if (!qb.expressionMap.aliases.find((alias) => alias.name === 'activeVersion')) {
qb.innerJoin('workflow.activeVersion', 'activeVersion');
}

const { whereClause, parameters } = buildWorkflowsByNodesQuery(
nodeTypes,
this.globalConfig.database.type,
'activeVersion.nodes',
'activeVersion_',
);

qb.andWhere(whereClause, parameters);
}

private applyOwnedByRelation(qb: SelectQueryBuilder<WorkflowEntity>): void {
// Check if 'shared' join already exists from project filter
if (!qb.expressionMap.aliases.find((alias) => alias.name === 'shared')) {
Expand Down
44 changes: 36 additions & 8 deletions packages/@n8n/db/src/utils/build-workflows-by-nodes-query.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,78 @@
/**
* Builds the WHERE clause and parameters for a query to find workflows by node types
* Quotes a column reference for use in raw SQL based on database type.
* Handles table.column format by quoting each part separately.
*/
function quoteColumnRef(
column: string,
dbType: 'postgresdb' | 'mysqldb' | 'mariadb' | 'sqlite',
): string {
if (!column.includes('.')) {
return column;
}

const [table, col] = column.split('.');
if (dbType === 'mysqldb' || dbType === 'mariadb') {
return `\`${table}\`.\`${col}\``;
}
// PostgreSQL and SQLite use double quotes
return `"${table}"."${col}"`;
}

/**
* Builds the WHERE clause and parameters for a query to find workflows by node types.
* @param nodeTypes - Array of node types to search for
* @param dbType - Database type
* @param nodesColumn - The column to search in (default: 'workflow.nodes')
* @param paramPrefix - Prefix for parameter names to avoid conflicts when used multiple times (default: '')
*/
export function buildWorkflowsByNodesQuery(
nodeTypes: string[],
dbType: 'postgresdb' | 'mysqldb' | 'mariadb' | 'sqlite',
nodesColumn: string = 'workflow.nodes',
paramPrefix: string = '',
) {
let whereClause: string;

const parameters: Record<string, string | string[]> = { nodeTypes };
const nodeTypesParam = `${paramPrefix}nodeTypes`;
const parameters: Record<string, string | string[]> = { [nodeTypesParam]: nodeTypes };
const quotedColumn = quoteColumnRef(nodesColumn, dbType);

switch (dbType) {
case 'postgresdb':
whereClause = `EXISTS (
SELECT 1
FROM jsonb_array_elements(workflow.nodes::jsonb) AS node
WHERE node->>'type' = ANY(:nodeTypes)
FROM jsonb_array_elements(${quotedColumn}::jsonb) AS node
WHERE node->>'type' = ANY(:${nodeTypesParam})
)`;
break;
case 'mysqldb':
case 'mariadb': {
const conditions = nodeTypes
.map(
(_, i) =>
`JSON_SEARCH(JSON_EXTRACT(workflow.nodes, '$[*].type'), 'one', :nodeType${i}) IS NOT NULL`,
`JSON_SEARCH(JSON_EXTRACT(${quotedColumn}, '$[*].type'), 'one', :${paramPrefix}nodeType${i}) IS NOT NULL`,
)
.join(' OR ');

whereClause = `(${conditions})`;

nodeTypes.forEach((nodeType, index) => {
parameters[`nodeType${index}`] = nodeType;
parameters[`${paramPrefix}nodeType${index}`] = nodeType;
});
break;
}
case 'sqlite': {
const conditions = nodeTypes
.map(
(_, i) =>
`EXISTS (SELECT 1 FROM json_each(workflow.nodes) WHERE json_extract(json_each.value, '$.type') = :nodeType${i})`,
`EXISTS (SELECT 1 FROM json_each(${quotedColumn}) WHERE json_extract(json_each.value, '$.type') = :${paramPrefix}nodeType${i})`,
)
.join(' OR ');

whereClause = `(${conditions})`;

nodeTypes.forEach((nodeType, index) => {
parameters[`nodeType${index}`] = nodeType;
parameters[`${paramPrefix}nodeType${index}`] = nodeType;
});
break;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/middlewares/list-query/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const filterListQueryMiddleware = async (

let Filter;

if (req.baseUrl.endsWith('workflows')) {
if (req.baseUrl.endsWith('workflows') || req.path.endsWith('workflows')) {
Filter = WorkflowFilter;
} else if (req.baseUrl.endsWith('credentials')) {
Filter = CredentialsFilter;
Expand Down
29 changes: 29 additions & 0 deletions packages/cli/src/modules/mcp/mcp.settings.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { findMcpSupportedTrigger } from './mcp.utils';

import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { listQueryMiddleware } from '@/middlewares';
import type { ListQuery } from '@/requests';
import { WorkflowFinderService } from '@/workflows/workflow-finder.service';
import { WorkflowService } from '@/workflows/workflow.service';

Expand Down Expand Up @@ -66,6 +68,33 @@ export class McpSettingsController {
return await this.mcpServerApiKeyService.rotateMcpServerApiKey(req.user);
}

@Get('/workflows', { middlewares: listQueryMiddleware })
async getMcpEligibleWorkflows(req: ListQuery.Request, res: Response) {
const supportedTriggerNodeTypes = Object.keys(SUPPORTED_MCP_TRIGGERS);

const options: ListQuery.Options = {
...req.listQueryOptions,
filter: {
...req.listQueryOptions?.filter,
active: true,
isArchived: false,
activeVersionNodeTypes: supportedTriggerNodeTypes,
availableInMCP: false,
},
};

const { workflows, count } = await this.workflowService.getMany(
req.user,
options,
false, // includeScopes
false, // includeFolders
false, // onlySharedWithMe
['workflow:update'], // requiredScopes - only return workflows the user can edit
);

res.json({ count, data: workflows });
}

@ProjectScope('workflow:update')
@Patch('/workflows/:workflowId/toggle-access')
async toggleWorkflowMCPAccess(
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/workflows/workflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class WorkflowService {
includeScopes?: boolean,
includeFolders?: boolean,
onlySharedWithMe?: boolean,
requiredScopes: Scope[] = ['workflow:read'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick(non-blocking): Should we document this so when the people the the method the know what requiredScopes is 🤔 ?

) {
let count;
let workflows;
Expand All @@ -102,7 +103,7 @@ export class WorkflowService {
sharedWorkflowIds = await this.workflowSharingService.getSharedWithMeIds(user);
} else {
sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
scopes: ['workflow:read'],
scopes: requiredScopes,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,5 @@ export const HtmlEdgeCase = PropTemplate.bind({});
HtmlEdgeCase.args = {
theme: 'warning',
content:
'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod <a href="">read the documentation</a> ut labore et dolore magna aliqua.',
'This content is long and will be truncated at 150 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod <a href="">read the documentation</a> ut labore et dolore magna aliqua. <ul><li>Item 1</li><li>Item 2</li></ul>',
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const displayContent = computed(() =>
'data-action-parameter-creatorview',
],
},
allowedTags: ['ul', 'li'],
}),
);

Expand Down Expand Up @@ -101,6 +102,15 @@ const onClick = (event: MouseEvent) => {
a {
font-weight: var(--font-weight--bold);
}

ul {
padding-left: var(--spacing--lg);
margin: var(--spacing--xs) 0;
}

li {
margin-bottom: var(--spacing--4xs);
}
}

.warning {
Expand Down
6 changes: 6 additions & 0 deletions packages/frontend/@n8n/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2391,6 +2391,12 @@
"settings.mcp.oAuthClients.revoke.success.message": "Client {name} access has been revoked",
"settings.mcp.oAuthClients.revoke.error": "Error revoking client access",
"settings.mcp.refresh.tooltip": "Refresh list",
"settings.mcp.connectWorkflows": "Connect workflows",
"settings.mcp.connectWorkflows.notice": "Workflows that are published and have one of webhook, form, schedule or chat trigger nodes can be enabled for MCP access",
"settings.mcp.connectWorkflows.input.placeholder": "Search workflows to connect",
"settings.mcp.connectWorkflows.confirm.label": "Connect",
"settings.mcp.connectWorkflows.error": "Error fetching available workflows",
"settings.mcp.connectWorkflows.emptyState": "No workflows found",
"settings.mcp.connectPopover.tab.oauth": "OAuth",
"settings.mcp.connectPopover.tab.accessToken": "Access token",
"settings.mcp.connectPopover.serverUrl": "Server URL",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1158,19 +1158,6 @@ onBeforeUnmount(() => {
background-color: var(--color--background--light-2);
}

.time-saved-input {
display: flex;
align-items: center;

:global(.el-input) {
width: var(--spacing--3xl);
}

span {
margin-left: var(--spacing--2xs);
}
}

.time-saved-warning {
color: var(--color--text);
line-height: var(--line-height--xl);
Expand Down
Loading
Loading