fix(core): Sort MCP search_workflows by most recently edited (#31245)

This commit is contained in:
Ricardo Espinoza 2026-05-29 04:35:56 -04:00 committed by GitHub
parent e07c8e6e6d
commit 3d452f7cb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 53 additions and 3 deletions

View File

@ -145,6 +145,28 @@ describe('search-workflows MCP tool', () => {
});
});
test('defaults to sorting by most recently updated first', async () => {
const workflowService = mockInstance(WorkflowService, {
getMany: jest.fn().mockResolvedValue({ workflows: [], count: 0 }),
});
await searchWorkflows(user, workflowService as unknown as WorkflowService, {});
const [, optionsArg] = (workflowService.getMany as jest.Mock).mock.calls[0];
expect(optionsArg.sortBy).toBe('updatedAt:desc');
});
test('passes through explicit sortBy option', async () => {
const workflowService = mockInstance(WorkflowService, {
getMany: jest.fn().mockResolvedValue({ workflows: [], count: 0 }),
});
await searchWorkflows(user, workflowService as unknown as WorkflowService, {
sortBy: 'name:asc',
});
const [, optionsArg] = (workflowService.getMany as jest.Mock).mock.calls[0];
expect(optionsArg.sortBy).toBe('name:asc');
});
test('clamps non-positive limit up to 1', async () => {
const workflowService = mockInstance(WorkflowService, {
getMany: jest.fn().mockResolvedValue({ workflows: [], count: 0 }),

View File

@ -24,10 +24,22 @@ export type ToolDefinition<InputArgs extends z.ZodRawShape = z.ZodRawShape> = {
};
// Shared MCP tool types
export const SEARCH_WORKFLOWS_SORT_BY_VALUES = [
'updatedAt:desc',
'updatedAt:asc',
'createdAt:desc',
'createdAt:asc',
'name:asc',
'name:desc',
] as const;
export type SearchWorkflowsSortBy = (typeof SEARCH_WORKFLOWS_SORT_BY_VALUES)[number];
export type SearchWorkflowsParams = {
limit?: number;
query?: string;
projectId?: string;
sortBy?: SearchWorkflowsSortBy;
};
export type SearchWorkflowsItem = {

View File

@ -2,11 +2,13 @@ import { type User, type WorkflowEntity } from '@n8n/db';
import z from 'zod';
import { USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants';
import { SEARCH_WORKFLOWS_SORT_BY_VALUES } from '../mcp.types';
import type {
ToolDefinition,
SearchWorkflowsParams,
SearchWorkflowsResult,
SearchWorkflowsItem,
SearchWorkflowsSortBy,
UserCalledMCPToolEventPayload,
} from '../mcp.types';
@ -17,10 +19,18 @@ import { createLimitSchema } from './schemas';
const MAX_RESULTS = 200;
const DEFAULT_SORT_BY: SearchWorkflowsSortBy = 'updatedAt:desc';
const inputSchema = {
limit: createLimitSchema(MAX_RESULTS),
query: z.string().optional().describe('Filter by name or description'),
projectId: z.string().optional(),
sortBy: z
.enum(SEARCH_WORKFLOWS_SORT_BY_VALUES)
.optional()
.describe(
`Sort order for results (default: ${DEFAULT_SORT_BY}). Use updatedAt:desc to find the most recently edited workflows first.`,
),
} satisfies z.ZodRawShape;
const outputSchema = {
@ -38,7 +48,9 @@ const outputSchema = {
updatedAt: z
.string()
.nullable()
.describe('The ISO timestamp when the workflow was last updated'),
.describe(
'ISO timestamp the workflow definition was last saved. Use this to identify recently edited workflows.',
),
triggerCount: z
.number()
.nullable()
@ -82,12 +94,14 @@ export const createSearchWorkflowsTool = (
limit = MAX_RESULTS,
query,
projectId,
sortBy,
}: {
limit?: number;
query?: string;
projectId?: string;
sortBy?: SearchWorkflowsSortBy;
}) => {
const parameters = { limit, query, projectId };
const parameters = { limit, query, projectId, sortBy };
const telemetryPayload: UserCalledMCPToolEventPayload = {
user_id: user.id,
tool_name: 'search_workflows',
@ -99,6 +113,7 @@ export const createSearchWorkflowsTool = (
limit,
query,
projectId,
sortBy,
});
// Track successful execution
@ -136,12 +151,13 @@ export const createSearchWorkflowsTool = (
export async function searchWorkflows(
user: User,
workflowService: WorkflowService,
{ limit = MAX_RESULTS, query, projectId }: SearchWorkflowsParams,
{ limit = MAX_RESULTS, query, projectId, sortBy = DEFAULT_SORT_BY }: SearchWorkflowsParams,
): Promise<SearchWorkflowsResult> {
const safeLimit = Math.min(Math.max(1, limit), MAX_RESULTS);
const options: ListQuery.Options = {
take: safeLimit,
sortBy,
filter: {
isArchived: false,
...(query ? { query } : {}),