From cd2bda59845490c1349bda302e8c7a417f588eee Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 09:48:15 +0000 Subject: [PATCH] feat(core): Add detailLevel option to MCP get_workflow_details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional detailLevel input ('full' | 'execution', default 'full') to the get_workflow_details MCP tool. With 'execution', the response omits the graph fields (nodes, connections, nodeGroups, activeVersion, settings, meta) and returns only workflow metadata plus trigger info — enough to run the workflow via execute_workflow at a fraction of the response size. 'execution' also skips loading the active version relation from the database. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01BW82a6VEyiYjRxtiz8VzzT --- .../get-workflow-details.tool.test.ts | 47 ++++++++++++++++++- packages/cli/src/modules/mcp/mcp.types.ts | 2 +- .../mcp/tools/get-workflow-details.tool.ts | 47 ++++++++++++++----- packages/cli/src/modules/mcp/tools/schemas.ts | 18 ++++--- 4 files changed, 93 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/modules/mcp/__tests__/get-workflow-details.tool.test.ts b/packages/cli/src/modules/mcp/__tests__/get-workflow-details.tool.test.ts index 713b49965fd..18a5daf187d 100644 --- a/packages/cli/src/modules/mcp/__tests__/get-workflow-details.tool.test.ts +++ b/packages/cli/src/modules/mcp/__tests__/get-workflow-details.tool.test.ts @@ -91,7 +91,7 @@ describe('get-workflow-details MCP tool', () => { ); expect('pinData' in payload.workflow).toBe(false); - expect(payload.workflow.nodes.every((n) => !('credentials' in n))).toBe(true); + expect(payload.workflow.nodes?.every((n) => !('credentials' in n))).toBe(true); expect(payload.triggerInfo).toContain('MOCK_TRIGGER_DETAILS'); expect(payload.workflow.versionId).toBe(workflow.versionId); expect(payload.workflow.activeVersionId).toBe(workflow.activeVersionId); @@ -101,6 +101,51 @@ describe('get-workflow-details MCP tool', () => { expect(payload.workflow.canExecute).toBe(true); }); + test("omits graph fields when detailLevel is 'execution'", async () => { + const workflow = createWorkflow({ activeVersionId: uuid() }); + const findWorkflowForUser = vi.fn().mockResolvedValue(workflow); + const workflowFinderService = mockInstance(WorkflowFinderService, { + findWorkflowForUser, + }); + const credentialsService = mockInstance(CredentialsService, {}); + const endpoints = { webhook: 'webhook', webhookTest: 'webhook-test' }; + + const payload = await getWorkflowDetails( + user, + baseWebhookUrl, + workflowFinderService, + credentialsService, + nodeTypes, + endpoints, + roleService, + projectService, + { workflowId: 'wf-1', detailLevel: 'execution' }, + ); + + // Skips loading the active version relation entirely + expect(findWorkflowForUser).toHaveBeenCalledWith( + 'wf-1', + user, + ['workflow:read'], + expect.objectContaining({ includeActiveVersion: false }), + ); + + expect(payload.workflow.nodes).toBeUndefined(); + expect(payload.workflow.connections).toBeUndefined(); + expect(payload.workflow.nodeGroups).toBeUndefined(); + expect(payload.workflow.activeVersion).toBeUndefined(); + expect(payload.workflow.settings).toBeUndefined(); + expect(payload.workflow.meta).toBeUndefined(); + + // Everything needed to execute is still present + expect(payload.triggerInfo).toContain('MOCK_TRIGGER_DETAILS'); + expect(payload.workflow.id).toBe('wf-1'); + expect(payload.workflow.active).toBe(true); + expect(payload.workflow.scopes).toEqual(['workflow:read', 'workflow:execute']); + expect(payload.workflow.canExecute).toBe(true); + expect(payload.workflow.description).toBeUndefined(); + }); + test('presents node groups by member node names, dropping stale ids', async () => { const workflow = createWorkflow({ nodeGroups: [{ id: 'group-1', name: 'Intake', nodeIds: ['node-1', 'stale-id'] }], diff --git a/packages/cli/src/modules/mcp/mcp.types.ts b/packages/cli/src/modules/mcp/mcp.types.ts index 2bc6192091c..bf13b43eec5 100644 --- a/packages/cli/src/modules/mcp/mcp.types.ts +++ b/packages/cli/src/modules/mcp/mcp.types.ts @@ -70,7 +70,7 @@ export type SearchWorkflowsResult = { export type WorkflowDetailsResult = z.infer; export type WorkflowDetailsWorkflow = WorkflowDetailsResult['workflow']; -export type WorkflowDetailsNode = WorkflowDetailsWorkflow['nodes'][number]; +export type WorkflowDetailsNode = NonNullable[number]; // JSON-RPC types for MCP protocol export type JSONRPCRequest = { diff --git a/packages/cli/src/modules/mcp/tools/get-workflow-details.tool.ts b/packages/cli/src/modules/mcp/tools/get-workflow-details.tool.ts index 841c4eb31c1..89a6881ffd8 100644 --- a/packages/cli/src/modules/mcp/tools/get-workflow-details.tool.ts +++ b/packages/cli/src/modules/mcp/tools/get-workflow-details.tool.ts @@ -20,8 +20,16 @@ import { getMcpWorkflow } from './workflow-validation.utils'; const inputSchema = { workflowId: z.string().describe('The ID of the workflow to retrieve'), + detailLevel: z + .enum(['full', 'execution']) + .optional() + .describe( + "Level of detail to return. 'full' (default) includes the complete workflow graph (nodes, connections, node groups, active version). 'execution' omits the graph and returns only the workflow metadata and trigger information needed to run it — prefer it when the goal is just to execute the workflow via execute_workflow.", + ), } satisfies z.ZodRawShape; +export type WorkflowDetailsLevel = NonNullable>; + export type WorkflowDetailsOutputSchema = typeof workflowDetailsOutputSchema; const outputSchema = workflowDetailsOutputSchema.shape satisfies z.ZodRawShape; @@ -55,8 +63,14 @@ export const createWorkflowDetailsTool = ( openWorldHint: false, // Works with internal n8n data only }, }, - handler: async ({ workflowId }: { workflowId: string }) => { - const parameters = { workflowId }; + handler: async ({ + workflowId, + detailLevel = 'full', + }: { + workflowId: string; + detailLevel?: WorkflowDetailsLevel; + }) => { + const parameters = { workflowId, detailLevel }; const telemetryPayload: UserCalledMCPToolEventPayload = { user_id: user.id, tool_name: 'get_workflow_details', @@ -73,7 +87,7 @@ export const createWorkflowDetailsTool = ( endpoints, roleService, projectService, - { workflowId }, + { workflowId, detailLevel }, testBaseWebhookUrl, ); @@ -84,7 +98,7 @@ export const createWorkflowDetailsTool = ( workflow_id: workflowId, workflow_name: payload.workflow.name, trigger_count: payload.workflow.triggerCount, - node_count: payload.workflow.nodes.length, + node_count: payload.workflow.nodes?.length, }, }; telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload); @@ -115,15 +129,16 @@ export async function getWorkflowDetails( endpoints: WebhookEndpoints, roleService: RoleService, projectService: ProjectService, - { workflowId }: { workflowId: string }, + { workflowId, detailLevel = 'full' }: { workflowId: string; detailLevel?: WorkflowDetailsLevel }, testBaseWebhookUrl: string = baseWebhookUrl, ): Promise { + const includeGraph = detailLevel === 'full'; const workflow = await getMcpWorkflow( workflowId, user, ['workflow:read'], workflowFinderService, - { includeActiveVersion: true, includeTags: true }, + { includeActiveVersion: includeGraph, includeTags: true }, ); // Compute user scopes for this workflow @@ -135,7 +150,7 @@ export async function getWorkflowDetails( const nodes = workflow.nodes ?? []; const connections = workflow.connections ?? {}; const activeVersion = - workflow.activeVersionId && workflow.activeVersion + includeGraph && workflow.activeVersionId && workflow.activeVersion ? { nodes: (workflow.activeVersion.nodes ?? []).map( ({ credentials: _credentials, ...node }) => node, @@ -174,17 +189,23 @@ export async function getWorkflowDetails( triggerCount: workflow.triggerCount, createdAt: workflow.createdAt.toISOString(), updatedAt: workflow.updatedAt.toISOString(), - settings: workflow.settings ?? null, - connections, - nodes: nodes.map(({ credentials: _credentials, ...node }) => node), - nodeGroups: toNodeGroupSummary(workflow.nodeGroups ?? [], nodes), - activeVersion, tags: toTagSummary(workflow.tags), - meta: workflow.meta ?? null, parentFolderId: workflow.parentFolder?.id ?? null, description: workflow.description ?? undefined, scopes, canExecute, + // The graph fields dominate the response size; skip them when the caller + // only needs enough to execute the workflow. + ...(includeGraph + ? { + settings: workflow.settings ?? null, + connections, + nodes: nodes.map(({ credentials: _credentials, ...node }) => node), + nodeGroups: toNodeGroupSummary(workflow.nodeGroups ?? [], nodes), + activeVersion, + meta: workflow.meta ?? null, + } + : {}), }; return { workflow: sanitizedWorkflow, triggerInfo: triggerNotice }; diff --git a/packages/cli/src/modules/mcp/tools/schemas.ts b/packages/cli/src/modules/mcp/tools/schemas.ts index 37a18e8f029..ae78ef4c9d2 100644 --- a/packages/cli/src/modules/mcp/tools/schemas.ts +++ b/packages/cli/src/modules/mcp/tools/schemas.ts @@ -112,6 +112,8 @@ export const toNodeGroupSummary = ( })); }; +const GRAPH_FIELD_OMISSION_NOTE = "Omitted when detailLevel is 'execution'."; + export const workflowDetailsOutputSchema = z.object({ workflow: z .object({ @@ -127,10 +129,13 @@ export const workflowDetailsOutputSchema = z.object({ triggerCount: z.number(), createdAt: z.string().nullable(), updatedAt: z.string().nullable(), - settings: workflowSettingsSchema, - connections: z.record(z.unknown()), - nodes: z.array(nodeSchema), - nodeGroups: z.array(nodeGroupSchema).describe('Node groups in the workflow'), + settings: workflowSettingsSchema.optional().describe(GRAPH_FIELD_OMISSION_NOTE), + connections: z.record(z.unknown()).optional().describe(GRAPH_FIELD_OMISSION_NOTE), + nodes: z.array(nodeSchema).optional().describe(GRAPH_FIELD_OMISSION_NOTE), + nodeGroups: z + .array(nodeGroupSchema) + .optional() + .describe(`Node groups in the workflow. ${GRAPH_FIELD_OMISSION_NOTE}`), activeVersion: z .object({ nodes: z.array(nodeSchema), @@ -138,9 +143,10 @@ export const workflowDetailsOutputSchema = z.object({ nodeGroups: z.array(nodeGroupSchema).describe('Node groups in the active version'), }) .nullable() - .describe('Active workflow graph, if available'), + .optional() + .describe(`Active workflow graph, if available. ${GRAPH_FIELD_OMISSION_NOTE}`), tags: z.array(tagSchema), - meta: workflowMetaSchema, + meta: workflowMetaSchema.optional().describe(GRAPH_FIELD_OMISSION_NOTE), parentFolderId: z.string().nullable(), description: z.string().optional().describe('The description of the workflow'), scopes: z.array(z.string()).describe('User permissions for this workflow'),