n8n
Version:
n8n Workflow Automation Tool
178 lines • 8.67 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTestWorkflowTool = void 0;
exports.testWorkflow = testWorkflow;
const constants_1 = require("@n8n/constants");
const workflow_sdk_1 = require("@n8n/workflow-sdk");
const n8n_workflow_1 = require("n8n-workflow");
const zod_1 = __importDefault(require("zod"));
const mcp_constants_1 = require("../mcp.constants");
const mcp_errors_1 = require("../mcp.errors");
const execution_utils_1 = require("./execution-utils");
const workflow_validation_utils_1 = require("./workflow-validation.utils");
const inputSchema = zod_1.default.object({
workflowId: zod_1.default.string().describe('The ID of the workflow to test'),
pinData: zod_1.default
.record(zod_1.default.array(zod_1.default.record(zod_1.default.unknown())))
.describe('Pin data for all workflow nodes. Use the prepare_test_pin_data tool to generate this. Keys are node names, values are arrays of items. Each item MUST be wrapped in a "json" property, e.g. [{"json": {"id": "123", "name": "test"}}]. Do NOT pass flat objects like [{"id": "123"}].'),
triggerNodeName: zod_1.default
.string()
.optional()
.describe('Optional name of the trigger node to start execution from. Useful for workflows with multiple triggers. Defaults to the first trigger node found.'),
});
const outputSchema = {
executionId: zod_1.default.string().nullable(),
status: zod_1.default
.enum(['success', 'error', 'running', 'waiting', 'canceled', 'crashed', 'new', 'unknown'])
.describe('The status of the test execution'),
error: zod_1.default.string().optional().describe('Error message if the execution failed'),
};
const createTestWorkflowTool = (user, workflowFinderService, activeExecutions, workflowRunner, nodeTypes, telemetry, mcpService) => ({
name: 'test_workflow',
config: {
description: 'Test a workflow using pin data to bypass external services. Trigger nodes, nodes with credentials, and HTTP Request nodes are pinned (use simulated data). Other nodes (Set, If, Code, etc.) execute normally — including credential-free I/O nodes like Execute Command or file read/write nodes. Use prepare_test_pin_data to generate the pin data first.',
inputSchema: inputSchema.shape,
outputSchema,
annotations: {
title: 'Test Workflow',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: false,
},
},
handler: async ({ workflowId, pinData, triggerNodeName }) => {
const telemetryPayload = {
user_id: user.id,
tool_name: 'test_workflow',
parameters: {
workflowId,
nodeCount: Object.keys(pinData).length,
hasTriggerNodeName: !!triggerNodeName,
},
};
try {
const output = await testWorkflow(user, workflowFinderService, activeExecutions, workflowRunner, nodeTypes, mcpService, workflowId, pinData, triggerNodeName);
telemetryPayload.results = {
success: output.status === 'success',
data: { executionId: output.executionId, status: output.status },
};
if (output.status === 'error' && output.error) {
telemetryPayload.results.error = output.error;
}
telemetry.track(mcp_constants_1.USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
return {
content: [{ type: 'text', text: (0, n8n_workflow_1.jsonStringify)(output) }],
structuredContent: output,
};
}
catch (er) {
const error = (0, n8n_workflow_1.ensureError)(er);
const isTimeout = error instanceof mcp_errors_1.McpExecutionTimeoutError;
const isAccessError = error instanceof mcp_errors_1.WorkflowAccessError;
const output = {
executionId: isTimeout ? error.executionId : null,
status: 'error',
error: isTimeout
? `Workflow execution timed out after ${execution_utils_1.WORKFLOW_EXECUTION_TIMEOUT_MS * constants_1.Time.milliseconds.toSeconds} seconds`
: (error.message ?? `${error.constructor.name}: (no message)`),
};
telemetryPayload.results = {
success: false,
error: isTimeout ? 'Workflow execution timed out' : error.message,
error_reason: isAccessError ? error.reason : undefined,
};
telemetry.track(mcp_constants_1.USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);
return {
content: [{ type: 'text', text: (0, n8n_workflow_1.jsonStringify)(output) }],
structuredContent: output,
};
}
},
});
exports.createTestWorkflowTool = createTestWorkflowTool;
async function testWorkflow(user, workflowFinderService, activeExecutions, workflowRunner, nodeTypes, mcpService, workflowId, pinData, triggerNodeName) {
const workflow = await (0, workflow_validation_utils_1.getMcpWorkflow)(workflowId, user, ['workflow:execute'], workflowFinderService);
const nodes = workflow.nodes ?? [];
const connections = workflow.connections ?? {};
const triggerNode = findTriggerNode(nodes, nodeTypes, triggerNodeName);
if (!triggerNode) {
throw new mcp_errors_1.WorkflowAccessError(triggerNodeName
? `Trigger node "${triggerNodeName}" not found in the workflow. Check the node name and ensure it is a trigger node.`
: 'Workflow has no trigger node. A trigger node is required to test the workflow.', 'unsupported_trigger');
}
const nodeNames = new Set(nodes.map((n) => n.name));
const unknownKeys = Object.keys(pinData).filter((key) => !nodeNames.has(key));
if (unknownKeys.length > 0) {
throw new mcp_errors_1.WorkflowAccessError(`Pin data contains unknown node names: ${unknownKeys.join(', ')}. Check for typos — node names must match exactly.`, 'invalid_pin_data');
}
const normalizedPinData = (0, workflow_sdk_1.normalizePinData)(pinData);
const triggerPinData = normalizedPinData[triggerNode.name] ?? [
{ json: {} },
];
const mcpMessageId = `mcp-test-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
const runData = {
executionMode: 'manual',
workflowData: { ...workflow, nodes, connections },
userId: user.id,
isMcpExecution: mcpService.isQueueMode,
mcpType: 'service',
mcpSessionId: mcpMessageId,
mcpMessageId,
startNodes: [{ name: triggerNode.name, sourceData: null }],
pinData: normalizedPinData,
executionData: (0, n8n_workflow_1.createRunExecutionData)({
startData: {},
resultData: {
pinData: normalizedPinData,
runData: {},
},
executionData: {
contextData: {},
metadata: {},
nodeExecutionStack: [
{
node: triggerNode,
data: {
main: [triggerPinData],
},
source: null,
},
],
waitingExecution: {},
waitingExecutionSource: {},
},
}),
};
const executionId = await workflowRunner.run(runData);
const data = await (0, execution_utils_1.waitForExecutionResult)(executionId, activeExecutions, mcpService);
const hasError = data.status === 'error' || data.data.resultData?.error;
return {
executionId,
status: hasError ? 'error' : data.status,
error: hasError
? (data.data.resultData?.error?.message ?? 'Execution completed with errors')
: undefined,
};
}
function findTriggerNode(nodes, nodeTypes, triggerNodeName) {
for (const node of nodes) {
if (node.disabled)
continue;
if (triggerNodeName && node.name !== triggerNodeName)
continue;
try {
const nodeType = nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if ((0, n8n_workflow_1.isTriggerNode)(nodeType.description)) {
return node;
}
}
catch {
}
}
return undefined;
}
//# sourceMappingURL=test-workflow.tool.js.map