From 3d452f7cb92083f0afb0570bddaa21bce361b84b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 29 May 2026 04:35:56 -0400 Subject: [PATCH] fix(core): Sort MCP search_workflows by most recently edited (#31245) --- .../__tests__/search-workflows.tool.test.ts | 22 +++++++++++++++++++ packages/cli/src/modules/mcp/mcp.types.ts | 12 ++++++++++ .../mcp/tools/search-workflows.tool.ts | 22 ++++++++++++++++--- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/modules/mcp/__tests__/search-workflows.tool.test.ts b/packages/cli/src/modules/mcp/__tests__/search-workflows.tool.test.ts index afa93f30284..1582386104e 100644 --- a/packages/cli/src/modules/mcp/__tests__/search-workflows.tool.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/search-workflows.tool.test.ts @@ -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 }), diff --git a/packages/cli/src/modules/mcp/mcp.types.ts b/packages/cli/src/modules/mcp/mcp.types.ts index 9ca20a7a2c4..8809d7ad8de 100644 --- a/packages/cli/src/modules/mcp/mcp.types.ts +++ b/packages/cli/src/modules/mcp/mcp.types.ts @@ -24,10 +24,22 @@ export type ToolDefinition = { }; // 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 = { diff --git a/packages/cli/src/modules/mcp/tools/search-workflows.tool.ts b/packages/cli/src/modules/mcp/tools/search-workflows.tool.ts index a9e31c0334a..06d1e395527 100644 --- a/packages/cli/src/modules/mcp/tools/search-workflows.tool.ts +++ b/packages/cli/src/modules/mcp/tools/search-workflows.tool.ts @@ -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 { const safeLimit = Math.min(Math.max(1, limit), MAX_RESULTS); const options: ListQuery.Options = { take: safeLimit, + sortBy, filter: { isArchived: false, ...(query ? { query } : {}),