n8n-mcp 2.75.0 → 2.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +7 -5
  2. package/dist/http-server-single-session.d.ts.map +1 -1
  3. package/dist/http-server-single-session.js.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/mcp/agents-action-map.d.ts +1 -0
  9. package/dist/mcp/agents-action-map.d.ts.map +1 -1
  10. package/dist/mcp/agents-action-map.js +2 -1
  11. package/dist/mcp/agents-action-map.js.map +1 -1
  12. package/dist/mcp/handlers-n8n-manager.d.ts +3 -0
  13. package/dist/mcp/handlers-n8n-manager.d.ts.map +1 -1
  14. package/dist/mcp/handlers-n8n-manager.js +564 -103
  15. package/dist/mcp/handlers-n8n-manager.js.map +1 -1
  16. package/dist/mcp/handlers-official-tools.d.ts +17 -0
  17. package/dist/mcp/handlers-official-tools.d.ts.map +1 -1
  18. package/dist/mcp/handlers-official-tools.js +78 -34
  19. package/dist/mcp/handlers-official-tools.js.map +1 -1
  20. package/dist/mcp/server.d.ts.map +1 -1
  21. package/dist/mcp/server.js +10 -76
  22. package/dist/mcp/server.js.map +1 -1
  23. package/dist/mcp/tool-docs/workflow_management/n8n-manage-datatable.d.ts.map +1 -1
  24. package/dist/mcp/tool-docs/workflow_management/n8n-manage-datatable.js +31 -9
  25. package/dist/mcp/tool-docs/workflow_management/n8n-manage-datatable.js.map +1 -1
  26. package/dist/mcp/tool-docs/workflow_management/n8n-test-workflow.d.ts.map +1 -1
  27. package/dist/mcp/tool-docs/workflow_management/n8n-test-workflow.js +92 -19
  28. package/dist/mcp/tool-docs/workflow_management/n8n-test-workflow.js.map +1 -1
  29. package/dist/mcp/tool-docs/workflow_management/n8n-workflow-versions.d.ts.map +1 -1
  30. package/dist/mcp/tool-docs/workflow_management/n8n-workflow-versions.js +129 -28
  31. package/dist/mcp/tool-docs/workflow_management/n8n-workflow-versions.js.map +1 -1
  32. package/dist/mcp/tool-policy.d.ts +9 -0
  33. package/dist/mcp/tool-policy.d.ts.map +1 -0
  34. package/dist/mcp/tool-policy.js +138 -0
  35. package/dist/mcp/tool-policy.js.map +1 -0
  36. package/dist/mcp/tools-documentation.js +3 -3
  37. package/dist/mcp/tools-n8n-manager.d.ts +1 -0
  38. package/dist/mcp/tools-n8n-manager.d.ts.map +1 -1
  39. package/dist/mcp/tools-n8n-manager.js +103 -21
  40. package/dist/mcp/tools-n8n-manager.js.map +1 -1
  41. package/dist/services/mcp-exposure.d.ts +21 -0
  42. package/dist/services/mcp-exposure.d.ts.map +1 -0
  43. package/dist/services/mcp-exposure.js +131 -0
  44. package/dist/services/mcp-exposure.js.map +1 -0
  45. package/dist/services/n8n-official-mcp-client.d.ts.map +1 -1
  46. package/dist/services/n8n-official-mcp-client.js +25 -3
  47. package/dist/services/n8n-official-mcp-client.js.map +1 -1
  48. package/dist/services/workflow-versioning-service.d.ts +2 -1
  49. package/dist/services/workflow-versioning-service.d.ts.map +1 -1
  50. package/dist/services/workflow-versioning-service.js +8 -2
  51. package/dist/services/workflow-versioning-service.js.map +1 -1
  52. package/dist/triggers/index.d.ts +1 -1
  53. package/dist/triggers/index.d.ts.map +1 -1
  54. package/dist/triggers/index.js +2 -1
  55. package/dist/triggers/index.js.map +1 -1
  56. package/dist/triggers/trigger-detector.d.ts +3 -2
  57. package/dist/triggers/trigger-detector.d.ts.map +1 -1
  58. package/dist/triggers/trigger-detector.js +12 -0
  59. package/dist/triggers/trigger-detector.js.map +1 -1
  60. package/dist/types/n8n-api.d.ts +6 -0
  61. package/dist/types/n8n-api.d.ts.map +1 -1
  62. package/package.json +1 -1
@@ -74,6 +74,9 @@ exports.handleInsertRows = handleInsertRows;
74
74
  exports.handleUpdateRows = handleUpdateRows;
75
75
  exports.handleUpsertRows = handleUpsertRows;
76
76
  exports.handleDeleteRows = handleDeleteRows;
77
+ exports.handleAddColumn = handleAddColumn;
78
+ exports.handleDeleteColumn = handleDeleteColumn;
79
+ exports.handleRenameColumn = handleRenameColumn;
77
80
  exports.handleListCredentials = handleListCredentials;
78
81
  exports.handleGetCredential = handleGetCredential;
79
82
  exports.handleCreateCredential = handleCreateCredential;
@@ -112,6 +115,10 @@ const execution_processor_1 = require("../services/execution-processor");
112
115
  const npm_version_checker_1 = require("../utils/npm-version-checker");
113
116
  const mcp_input_normalizer_1 = require("../utils/mcp-input-normalizer");
114
117
  const official_mcp_access_1 = require("./official-mcp-access");
118
+ const handlers_official_tools_1 = require("./handlers-official-tools");
119
+ const mcp_exposure_1 = require("../services/mcp-exposure");
120
+ const tool_policy_1 = require("./tool-policy");
121
+ const agents_action_map_1 = require("./agents-action-map");
115
122
  let defaultApiClient = null;
116
123
  let lastDefaultConfigUrl = null;
117
124
  const cacheMutex = new cache_utils_1.CacheMutex();
@@ -298,6 +305,7 @@ const autofixWorkflowSchema = zod_1.z.object({
298
305
  });
299
306
  const testWorkflowSchema = zod_1.z.object({
300
307
  workflowId: zod_1.z.string(),
308
+ method: optionalEmptyAware(zod_1.z.enum(['auto', 'trigger', 'prepare', 'pinned', 'direct'])),
301
309
  triggerType: optionalEmptyAware(zod_1.z.enum(['webhook', 'form', 'chat'])),
302
310
  httpMethod: optionalEmptyAware(zod_1.z.enum(['GET', 'POST', 'PUT', 'DELETE'])),
303
311
  webhookPath: optionalEmptyAware(zod_1.z.string()),
@@ -307,6 +315,11 @@ const testWorkflowSchema = zod_1.z.object({
307
315
  headers: zod_1.z.record(zod_1.z.string()).optional(),
308
316
  timeout: zod_1.z.number().optional(),
309
317
  waitForResponse: zod_1.z.boolean().optional(),
318
+ exposeToMcp: zod_1.z.boolean().optional(),
319
+ timeoutMs: zod_1.z.number().int().min(agents_action_map_1.MIN_TIMEOUT_MS).max(agents_action_map_1.MAX_TIMEOUT_MS).optional(),
320
+ pinData: zod_1.z.record(zod_1.z.array(zod_1.z.unknown())).optional(),
321
+ triggerNodeName: optionalEmptyAware(zod_1.z.string()),
322
+ executionMode: optionalEmptyAware(zod_1.z.enum(['manual', 'production'])),
310
323
  });
311
324
  const listExecutionsSchema = zod_1.z.object({
312
325
  limit: zod_1.z.number().min(1).max(100).optional(),
@@ -340,14 +353,20 @@ const cancelTestRunSchema = zod_1.z.object({
340
353
  workflowId: testRunPathId,
341
354
  runId: testRunPathId,
342
355
  });
356
+ const versionIdValue = zod_1.z.union([zod_1.z.number().int(), zod_1.z.string().min(1)]);
343
357
  const workflowVersionsSchema = zod_1.z.object({
344
- mode: zod_1.z.enum(['list', 'get', 'rollback', 'delete', 'prune']),
358
+ mode: zod_1.z.enum(['list', 'get', 'rollback', 'delete', 'prune', 'diff']),
359
+ source: zod_1.z.enum(['local', 'native']).optional(),
345
360
  workflowId: zod_1.z.string().optional(),
346
- versionId: zod_1.z.number().optional(),
361
+ versionId: versionIdValue.optional(),
362
+ toVersionId: versionIdValue.optional(),
347
363
  limit: zod_1.z.number().default(10).optional(),
364
+ offset: zod_1.z.number().int().min(0).optional(),
348
365
  validateBefore: zod_1.z.boolean().default(true).optional(),
349
366
  deleteAll: zod_1.z.boolean().default(false).optional(),
350
367
  maxVersions: zod_1.z.number().default(10).optional(),
368
+ exposeToMcp: zod_1.z.boolean().optional(),
369
+ timeoutMs: zod_1.z.number().int().min(5000).max(600000).optional(),
351
370
  });
352
371
  async function handleCreateWorkflow(args, context) {
353
372
  try {
@@ -1173,16 +1192,151 @@ async function handleAutofixWorkflow(args, repository, context) {
1173
1192
  };
1174
1193
  }
1175
1194
  }
1195
+ const OFFICIAL_TEST_ACTION = 'test_workflow';
1196
+ const FAILED_RUN_STATUSES = new Set(['error', 'crashed', 'canceled']);
1197
+ const LEGACY_TIMEOUT_SCOPE_WARNING = 'timeout applies to the HTTP trigger path only; use timeoutMs for method prepare/pinned/direct';
1198
+ const OFFICIAL_TEST_METHODS = new Set(['prepare', 'pinned', 'direct']);
1176
1199
  async function handleTestWorkflow(args, context) {
1177
1200
  try {
1178
- const client = ensureApiConfigured(context);
1179
1201
  const input = testWorkflowSchema.parse(args);
1180
- const { detectTriggerFromWorkflow, ensureRegistryInitialized, TriggerRegistry, } = await Promise.resolve().then(() => __importStar(require('../triggers')));
1181
- await ensureRegistryInitialized();
1202
+ const method = input.method ?? 'auto';
1203
+ const apiClient = getN8nApiClient(context);
1204
+ const officialTimeoutMs = input.timeoutMs ?? (method === 'prepare' ? agents_action_map_1.DEFAULT_TIMEOUT_MS : agents_action_map_1.PINNED_TIMEOUT_MS);
1205
+ const routeOfficial = async (aliases, officialArgs, idempotent, resolvedMethod) => {
1206
+ const response = await (0, mcp_exposure_1.withMcpExposure)({
1207
+ apiClient,
1208
+ workflowId: input.workflowId,
1209
+ exposeToMcp: input.exposeToMcp,
1210
+ action: OFFICIAL_TEST_ACTION,
1211
+ toolName: 'n8n_test_workflow',
1212
+ context,
1213
+ }, () => (0, handlers_official_tools_1.callOfficialTool)(context, aliases, officialArgs, officialTimeoutMs, OFFICIAL_TEST_ACTION, idempotent));
1214
+ const decorated = { ...response, method: resolvedMethod, backend: 'official-mcp' };
1215
+ if (input.timeout !== undefined && decorated.success) {
1216
+ decorated.warnings = [...(decorated.warnings ?? []), LEGACY_TIMEOUT_SCOPE_WARNING];
1217
+ }
1218
+ return decorated;
1219
+ };
1220
+ if (method === 'pinned' && Object.keys(input.pinData ?? {}).length === 0) {
1221
+ return {
1222
+ success: false,
1223
+ code: 'INVALID_ARGS',
1224
+ method: 'pinned',
1225
+ error: 'pinData is required for method: pinned (keys are node names, values are arrays of items wrapped as { "json": { ... } }). Run method: prepare first to see which nodes need pinned data.',
1226
+ };
1227
+ }
1228
+ if ((method === 'auto' || method === 'trigger') && (0, tool_policy_1.isOperationDisabled)('n8n_test_workflow', 'trigger')) {
1229
+ return {
1230
+ success: false,
1231
+ code: 'OPERATION_DISABLED',
1232
+ method: 'trigger',
1233
+ backend: 'public-api',
1234
+ error: "Operation 'trigger' on tool 'n8n_test_workflow' is disabled by server policy.",
1235
+ details: { requestedMethod: method },
1236
+ };
1237
+ }
1238
+ const isPlainPrepare = method === 'prepare' && input.exposeToMcp !== true;
1239
+ if (!isPlainPrepare && !(0, mcp_exposure_1.publicApiMatchesContext)(context)) {
1240
+ return {
1241
+ success: false,
1242
+ code: 'NOT_CONFIGURED',
1243
+ method,
1244
+ backend: OFFICIAL_TEST_METHODS.has(method) ? 'official-mcp' : 'public-api',
1245
+ error: mcp_exposure_1.PUBLIC_API_CONTEXT_HINT,
1246
+ };
1247
+ }
1248
+ if (method === 'prepare') {
1249
+ return routeOfficial(['prepare_workflow_pin_data'], { workflowId: input.workflowId }, true, 'prepare');
1250
+ }
1251
+ const client = ensureApiConfigured(context);
1252
+ const { detectTriggerFromWorkflow, classifyTriggerNode, ensureRegistryInitialized, TriggerRegistry, } = await Promise.resolve().then(() => __importStar(require('../triggers')));
1182
1253
  const workflow = await client.getWorkflow(input.workflowId);
1254
+ const detection = detectTriggerFromWorkflow(workflow);
1255
+ const detectedNodeName = detection.trigger?.node.name;
1256
+ if (method === 'pinned') {
1257
+ const pinnedTriggerNode = input.triggerNodeName ?? detectedNodeName;
1258
+ const response = await routeOfficial(['test_workflow'], {
1259
+ workflowId: input.workflowId,
1260
+ pinData: input.pinData,
1261
+ ...(pinnedTriggerNode ? { triggerNodeName: pinnedTriggerNode } : {}),
1262
+ timeout: Math.max(1, Math.floor(officialTimeoutMs / 1000) - 5),
1263
+ }, false, 'pinned');
1264
+ if (!response.success)
1265
+ return response;
1266
+ const run = (response.data ?? {});
1267
+ const executionId = typeof run.executionId === 'string' ? run.executionId : undefined;
1268
+ const status = typeof run.status === 'string' ? run.status : undefined;
1269
+ if (status && FAILED_RUN_STATUSES.has(status)) {
1270
+ return {
1271
+ ...response,
1272
+ success: false,
1273
+ code: 'EXECUTION_FAILED',
1274
+ error: typeof run.error === 'string' ? run.error : `Run finished with status ${status}`,
1275
+ ...(executionId ? { executionId } : {}),
1276
+ };
1277
+ }
1278
+ return { ...response, ...(executionId ? { executionId } : {}) };
1279
+ }
1280
+ if (method === 'direct') {
1281
+ const namedNode = input.triggerNodeName
1282
+ ? (workflow.nodes ?? []).find(node => node.name === input.triggerNodeName)
1283
+ : undefined;
1284
+ if (input.triggerNodeName && !namedNode) {
1285
+ return {
1286
+ success: false,
1287
+ code: 'INVALID_ARGS',
1288
+ method: 'direct',
1289
+ backend: 'official-mcp',
1290
+ error: `triggerNodeName "${input.triggerNodeName}" is not a node of workflow ${input.workflowId}`,
1291
+ };
1292
+ }
1293
+ const triggerKind = input.triggerNodeName
1294
+ ? (namedNode ? classifyTriggerNode(namedNode) : null)
1295
+ : (detection.trigger?.type ?? null);
1296
+ let inputs;
1297
+ if (input.message !== undefined) {
1298
+ inputs = { chatInput: input.message };
1299
+ }
1300
+ else if (input.data && triggerKind === 'form') {
1301
+ inputs = { formData: input.data };
1302
+ }
1303
+ else if (input.data || input.headers || input.httpMethod) {
1304
+ inputs = {
1305
+ webhookData: {
1306
+ method: input.httpMethod ?? 'POST',
1307
+ ...(input.data ? { body: input.data } : {}),
1308
+ ...(input.headers ? { headers: input.headers } : {}),
1309
+ },
1310
+ };
1311
+ }
1312
+ const directTriggerNode = input.triggerNodeName ?? (inputs ? detectedNodeName : undefined);
1313
+ if (inputs && !directTriggerNode) {
1314
+ return {
1315
+ success: false,
1316
+ code: 'INVALID_ARGS',
1317
+ method: 'direct',
1318
+ error: 'triggerNodeName is required when inputs are given and no trigger node could be detected',
1319
+ details: { workflowId: input.workflowId, reason: detection.reason },
1320
+ };
1321
+ }
1322
+ const response = await routeOfficial(['execute_workflow'], {
1323
+ workflowId: input.workflowId,
1324
+ executionMode: input.executionMode ?? 'manual',
1325
+ ...(directTriggerNode ? { triggerNodeName: directTriggerNode } : {}),
1326
+ ...(inputs ? { inputs } : {}),
1327
+ }, false, 'direct');
1328
+ if (!response.success)
1329
+ return response;
1330
+ const executionId = response.data?.executionId;
1331
+ return {
1332
+ ...response,
1333
+ ...(typeof executionId === 'string' ? { executionId } : {}),
1334
+ hint: 'execute_workflow returns as soon as the run starts; poll n8n_executions with the executionId for the result.',
1335
+ };
1336
+ }
1337
+ await ensureRegistryInitialized();
1183
1338
  let triggerType = input.triggerType;
1184
1339
  let triggerInfo;
1185
- const detection = detectTriggerFromWorkflow(workflow);
1186
1340
  if (!triggerType) {
1187
1341
  if (detection.detected && detection.trigger) {
1188
1342
  triggerType = detection.trigger.type;
@@ -1192,10 +1346,13 @@ async function handleTestWorkflow(args, context) {
1192
1346
  return {
1193
1347
  success: false,
1194
1348
  error: 'Workflow cannot be triggered externally',
1349
+ method,
1350
+ backend: 'public-api',
1195
1351
  details: {
1196
1352
  workflowId: input.workflowId,
1197
1353
  reason: detection.reason,
1198
- hint: 'Only workflows with webhook, form, or chat triggers can be executed via the API. Add one of these trigger nodes to your workflow.',
1354
+ hint: 'Only workflows with webhook, form, or chat triggers can be executed via the API. Add one of these trigger nodes to your workflow.'
1355
+ + ' To run it anyway through n8n\'s MCP server, call again with method: direct (executionMode manual) or method: pinned with pinData from method: prepare — both need N8N_MCP_ACCESS_TOKEN.',
1199
1356
  },
1200
1357
  };
1201
1358
  }
@@ -1208,6 +1365,8 @@ async function handleTestWorkflow(args, context) {
1208
1365
  return {
1209
1366
  success: false,
1210
1367
  error: `Workflow does not have a ${triggerType} trigger`,
1368
+ method: 'trigger',
1369
+ backend: 'public-api',
1211
1370
  details: {
1212
1371
  workflowId: input.workflowId,
1213
1372
  requestedTrigger: triggerType,
@@ -1224,6 +1383,8 @@ async function handleTestWorkflow(args, context) {
1224
1383
  return {
1225
1384
  success: false,
1226
1385
  error: `No handler registered for trigger type: ${triggerType}`,
1386
+ method: 'trigger',
1387
+ backend: 'public-api',
1227
1388
  details: {
1228
1389
  supportedTypes: TriggerRegistry.getRegisteredTypes(),
1229
1390
  },
@@ -1233,6 +1394,8 @@ async function handleTestWorkflow(args, context) {
1233
1394
  return {
1234
1395
  success: false,
1235
1396
  error: 'Workflow must be active to trigger via this method',
1397
+ method: 'trigger',
1398
+ backend: 'public-api',
1236
1399
  details: {
1237
1400
  workflowId: input.workflowId,
1238
1401
  triggerType,
@@ -1244,6 +1407,8 @@ async function handleTestWorkflow(args, context) {
1244
1407
  return {
1245
1408
  success: false,
1246
1409
  error: 'Chat trigger requires a message parameter',
1410
+ method: 'trigger',
1411
+ backend: 'public-api',
1247
1412
  details: {
1248
1413
  hint: 'Provide message="your message" for chat triggers',
1249
1414
  },
@@ -1271,6 +1436,8 @@ async function handleTestWorkflow(args, context) {
1271
1436
  : response.error,
1272
1437
  executionId: response.executionId,
1273
1438
  workflowId: input.workflowId,
1439
+ method: 'trigger',
1440
+ backend: 'public-api',
1274
1441
  details: {
1275
1442
  triggerType,
1276
1443
  metadata: response.metadata,
@@ -1279,9 +1446,16 @@ async function handleTestWorkflow(args, context) {
1279
1446
  };
1280
1447
  }
1281
1448
  catch (error) {
1449
+ const raw = (args && typeof args === 'object' ? args : {});
1450
+ const rawMethod = typeof raw.method === 'string' && raw.method.trim() !== '' ? raw.method : 'auto';
1451
+ const label = {
1452
+ method: rawMethod,
1453
+ backend: OFFICIAL_TEST_METHODS.has(rawMethod) ? 'official-mcp' : 'public-api',
1454
+ };
1282
1455
  if (error instanceof zod_1.z.ZodError) {
1283
1456
  return {
1284
1457
  success: false,
1458
+ ...label,
1285
1459
  error: 'Invalid input',
1286
1460
  details: { errors: error.errors },
1287
1461
  };
@@ -1289,6 +1463,7 @@ async function handleTestWorkflow(args, context) {
1289
1463
  if (error instanceof n8n_errors_1.N8nApiError) {
1290
1464
  return {
1291
1465
  success: false,
1466
+ ...label,
1292
1467
  error: (0, n8n_errors_1.getUserFriendlyErrorMessage)(error),
1293
1468
  code: error.code,
1294
1469
  details: error.details,
@@ -1296,6 +1471,7 @@ async function handleTestWorkflow(args, context) {
1296
1471
  }
1297
1472
  return {
1298
1473
  success: false,
1474
+ ...label,
1299
1475
  error: error instanceof Error ? error.message : 'Unknown error occurred',
1300
1476
  };
1301
1477
  }
@@ -2110,146 +2286,322 @@ async function handleDiagnostic(request, context) {
2110
2286
  data: diagnostic
2111
2287
  };
2112
2288
  }
2113
- async function handleWorkflowVersions(args, repository, context) {
2114
- try {
2115
- const input = workflowVersionsSchema.parse(args);
2116
- if (process.env.ENABLE_MULTI_TENANT === 'true' && (0, instance_context_1.getInstanceScopeId)(context) === '') {
2289
+ const VERSIONS_ACTION = 'workflow_versions';
2290
+ const VERSIONS_TIMEOUT_MS = 30000;
2291
+ const NATIVE_VERSIONS_LIMIT_CAP = 50;
2292
+ const NATIVE_VALIDATION_NOTE = 'not available for native versions';
2293
+ function withDiffFormat(data, format) {
2294
+ return data && typeof data === 'object' && !Array.isArray(data)
2295
+ ? { ...data, format }
2296
+ : { diff: data, format };
2297
+ }
2298
+ const LOCAL_VERSION_ID_PATTERN = /^-?\d+$/;
2299
+ function parseLocalVersionId(value, field) {
2300
+ if (value === undefined)
2301
+ return { ok: true, value: undefined };
2302
+ if (typeof value === 'string' && value.trim() === '') {
2303
+ return { ok: false, error: `${field} must be an integer version id for source: local` };
2304
+ }
2305
+ if (typeof value === 'string' && !LOCAL_VERSION_ID_PATTERN.test(value.trim())) {
2306
+ return {
2307
+ ok: false,
2308
+ error: `${field} must be an integer version id for source: local (got ${JSON.stringify(value)}). n8n's own string version ids need source: native.`,
2309
+ };
2310
+ }
2311
+ const parsed = typeof value === 'number' ? value : Number(value.trim());
2312
+ if (!Number.isInteger(parsed)) {
2313
+ return {
2314
+ ok: false,
2315
+ error: `${field} must be an integer version id for source: local (got ${JSON.stringify(value)}). n8n's own string version ids need source: native.`,
2316
+ };
2317
+ }
2318
+ return { ok: true, value: parsed };
2319
+ }
2320
+ async function handleNativeWorkflowVersions(input, context) {
2321
+ const mode = input.mode;
2322
+ const label = (response) => ({
2323
+ ...response,
2324
+ mode,
2325
+ source: 'native',
2326
+ backend: 'official-mcp',
2327
+ });
2328
+ const invalid = (error) => label({ success: false, action: VERSIONS_ACTION, code: 'INVALID_ARGS', error });
2329
+ if (mode === 'delete' || mode === 'prune') {
2330
+ return label({
2331
+ success: false,
2332
+ action: VERSIONS_ACTION,
2333
+ code: 'MODE_NOT_SUPPORTED_FOR_SOURCE',
2334
+ error: `n8n's own version history cannot be ${mode === 'delete' ? 'deleted' : 'pruned'} through MCP; use source: local for n8n-mcp snapshots`,
2335
+ });
2336
+ }
2337
+ const workflowId = input.workflowId;
2338
+ if (!workflowId) {
2339
+ return invalid(`workflowId is required for source: native (mode: ${mode})`);
2340
+ }
2341
+ if (mode !== 'list' && input.versionId === undefined) {
2342
+ return invalid(`versionId is required for mode: ${mode}`);
2343
+ }
2344
+ if (mode === 'diff' && input.toVersionId === undefined) {
2345
+ return invalid('toVersionId is required for mode: diff');
2346
+ }
2347
+ const versionId = input.versionId === undefined ? undefined : String(input.versionId);
2348
+ const toVersionId = input.toVersionId === undefined ? undefined : String(input.toVersionId);
2349
+ let aliases;
2350
+ let officialArgs;
2351
+ let idempotent = true;
2352
+ switch (mode) {
2353
+ case 'list':
2354
+ aliases = ['get_workflow_history'];
2355
+ officialArgs = {
2356
+ workflowId,
2357
+ limit: Math.max(1, Math.min(NATIVE_VERSIONS_LIMIT_CAP, Math.floor(input.limit ?? 10))),
2358
+ offset: Math.max(0, Math.floor(input.offset ?? 0)),
2359
+ };
2360
+ break;
2361
+ case 'get':
2362
+ aliases = ['get_workflow_version'];
2363
+ officialArgs = { workflowId, versionId };
2364
+ break;
2365
+ case 'diff':
2366
+ aliases = ['get_workflow_versions_diff'];
2367
+ officialArgs = { workflowId, fromVersionId: versionId, toVersionId };
2368
+ break;
2369
+ case 'rollback':
2370
+ aliases = ['restore_workflow_version'];
2371
+ officialArgs = { workflowId, versionId };
2372
+ idempotent = false;
2373
+ break;
2374
+ default:
2375
+ return invalid(`Unknown mode: ${mode}`);
2376
+ }
2377
+ const response = await (0, mcp_exposure_1.withMcpExposure)({
2378
+ apiClient: getN8nApiClient(context),
2379
+ workflowId,
2380
+ exposeToMcp: input.exposeToMcp,
2381
+ action: VERSIONS_ACTION,
2382
+ toolName: 'n8n_workflow_versions',
2383
+ context,
2384
+ }, () => (0, handlers_official_tools_1.callOfficialTool)(context, aliases, officialArgs, input.timeoutMs ?? VERSIONS_TIMEOUT_MS, VERSIONS_ACTION, idempotent));
2385
+ const labelled = label(response);
2386
+ if (mode === 'diff' && labelled.success) {
2387
+ labelled.data = withDiffFormat(labelled.data, 'n8n');
2388
+ }
2389
+ if (mode === 'rollback' && labelled.success) {
2390
+ labelled.validation = NATIVE_VALIDATION_NOTE;
2391
+ }
2392
+ return labelled;
2393
+ }
2394
+ async function handleLocalWorkflowVersions(input, versionId, toVersionId, repository, context) {
2395
+ const client = getN8nApiClient(context);
2396
+ const versioningService = new workflow_versioning_service_1.WorkflowVersioningService(repository, client || undefined, (0, instance_context_1.getInstanceScopeId)(context));
2397
+ switch (input.mode) {
2398
+ case 'list': {
2399
+ if (!input.workflowId) {
2400
+ return {
2401
+ success: false,
2402
+ error: 'workflowId is required for list mode'
2403
+ };
2404
+ }
2405
+ const versions = await versioningService.getVersionHistory(input.workflowId, input.limit);
2117
2406
  return {
2118
- success: false,
2119
- error: 'Workflow version storage is not available for this tenant context'
2407
+ success: true,
2408
+ data: {
2409
+ workflowId: input.workflowId,
2410
+ versions,
2411
+ count: versions.length,
2412
+ message: `Found ${versions.length} version(s) for workflow ${input.workflowId}`
2413
+ }
2414
+ };
2415
+ }
2416
+ case 'get': {
2417
+ if (!versionId) {
2418
+ return {
2419
+ success: false,
2420
+ error: 'versionId is required for get mode'
2421
+ };
2422
+ }
2423
+ const version = await versioningService.getVersion(versionId);
2424
+ if (!version) {
2425
+ return {
2426
+ success: false,
2427
+ error: `Version ${versionId} not found`
2428
+ };
2429
+ }
2430
+ return {
2431
+ success: true,
2432
+ data: version
2120
2433
  };
2121
2434
  }
2122
- const client = getN8nApiClient(context);
2123
- const versioningService = new workflow_versioning_service_1.WorkflowVersioningService(repository, client || undefined, (0, instance_context_1.getInstanceScopeId)(context));
2124
- switch (input.mode) {
2125
- case 'list': {
2435
+ case 'rollback': {
2436
+ if (!input.workflowId) {
2437
+ return {
2438
+ success: false,
2439
+ error: 'workflowId is required for rollback mode'
2440
+ };
2441
+ }
2442
+ if (!client) {
2443
+ return {
2444
+ success: false,
2445
+ error: 'n8n API not configured. Cannot perform rollback without API access.'
2446
+ };
2447
+ }
2448
+ const result = await versioningService.restoreVersion(input.workflowId, versionId, input.validateBefore);
2449
+ return {
2450
+ success: result.success,
2451
+ data: result.success ? result : undefined,
2452
+ error: result.success ? undefined : result.message,
2453
+ details: result.success ? undefined : {
2454
+ validationErrors: result.validationErrors
2455
+ }
2456
+ };
2457
+ }
2458
+ case 'delete': {
2459
+ if (input.deleteAll) {
2126
2460
  if (!input.workflowId) {
2127
2461
  return {
2128
2462
  success: false,
2129
- error: 'workflowId is required for list mode'
2463
+ error: 'workflowId is required for deleteAll mode'
2130
2464
  };
2131
2465
  }
2132
- const versions = await versioningService.getVersionHistory(input.workflowId, input.limit);
2466
+ const result = await versioningService.deleteAllVersions(input.workflowId);
2133
2467
  return {
2134
2468
  success: true,
2135
2469
  data: {
2136
2470
  workflowId: input.workflowId,
2137
- versions,
2138
- count: versions.length,
2139
- message: `Found ${versions.length} version(s) for workflow ${input.workflowId}`
2471
+ deleted: result.deleted,
2472
+ message: result.message
2140
2473
  }
2141
2474
  };
2142
2475
  }
2143
- case 'get': {
2144
- if (!input.versionId) {
2145
- return {
2146
- success: false,
2147
- error: 'versionId is required for get mode'
2148
- };
2149
- }
2150
- const version = await versioningService.getVersion(input.versionId);
2151
- if (!version) {
2476
+ else {
2477
+ if (!versionId) {
2152
2478
  return {
2153
2479
  success: false,
2154
- error: `Version ${input.versionId} not found`
2480
+ error: 'versionId is required for single version delete'
2155
2481
  };
2156
2482
  }
2483
+ const result = await versioningService.deleteVersion(versionId);
2157
2484
  return {
2158
- success: true,
2159
- data: version
2485
+ success: result.success,
2486
+ data: result.success ? { message: result.message } : undefined,
2487
+ error: result.success ? undefined : result.message
2160
2488
  };
2161
2489
  }
2162
- case 'rollback': {
2163
- if (!input.workflowId) {
2164
- return {
2165
- success: false,
2166
- error: 'workflowId is required for rollback mode'
2167
- };
2168
- }
2169
- if (!client) {
2170
- return {
2171
- success: false,
2172
- error: 'n8n API not configured. Cannot perform rollback without API access.'
2173
- };
2174
- }
2175
- const result = await versioningService.restoreVersion(input.workflowId, input.versionId, input.validateBefore);
2490
+ }
2491
+ case 'prune': {
2492
+ if (!input.workflowId) {
2176
2493
  return {
2177
- success: result.success,
2178
- data: result.success ? result : undefined,
2179
- error: result.success ? undefined : result.message,
2180
- details: result.success ? undefined : {
2181
- validationErrors: result.validationErrors
2182
- }
2494
+ success: false,
2495
+ error: 'workflowId is required for prune mode'
2183
2496
  };
2184
2497
  }
2185
- case 'delete': {
2186
- if (input.deleteAll) {
2187
- if (!input.workflowId) {
2188
- return {
2189
- success: false,
2190
- error: 'workflowId is required for deleteAll mode'
2191
- };
2192
- }
2193
- const result = await versioningService.deleteAllVersions(input.workflowId);
2194
- return {
2195
- success: true,
2196
- data: {
2197
- workflowId: input.workflowId,
2198
- deleted: result.deleted,
2199
- message: result.message
2200
- }
2201
- };
2202
- }
2203
- else {
2204
- if (!input.versionId) {
2205
- return {
2206
- success: false,
2207
- error: 'versionId is required for single version delete'
2208
- };
2209
- }
2210
- const result = await versioningService.deleteVersion(input.versionId);
2211
- return {
2212
- success: result.success,
2213
- data: result.success ? { message: result.message } : undefined,
2214
- error: result.success ? undefined : result.message
2215
- };
2216
- }
2217
- }
2218
- case 'prune': {
2219
- if (!input.workflowId) {
2220
- return {
2221
- success: false,
2222
- error: 'workflowId is required for prune mode'
2223
- };
2498
+ const result = await versioningService.pruneVersions(input.workflowId, input.maxVersions || 10);
2499
+ return {
2500
+ success: true,
2501
+ data: {
2502
+ workflowId: input.workflowId,
2503
+ pruned: result.pruned,
2504
+ remaining: result.remaining,
2505
+ message: `Pruned ${result.pruned} old version(s), ${result.remaining} version(s) remaining`
2224
2506
  }
2225
- const result = await versioningService.pruneVersions(input.workflowId, input.maxVersions || 10);
2507
+ };
2508
+ }
2509
+ case 'diff': {
2510
+ if (!input.workflowId) {
2226
2511
  return {
2227
- success: true,
2228
- data: {
2229
- workflowId: input.workflowId,
2230
- pruned: result.pruned,
2231
- remaining: result.remaining,
2232
- message: `Pruned ${result.pruned} old version(s), ${result.remaining} version(s) remaining`
2233
- }
2512
+ success: false,
2513
+ error: 'workflowId is required for diff mode'
2234
2514
  };
2235
2515
  }
2236
- default:
2516
+ if (versionId === undefined || toVersionId === undefined) {
2237
2517
  return {
2238
2518
  success: false,
2239
- error: `Unknown mode: ${input.mode}`
2519
+ code: 'INVALID_ARGS',
2520
+ error: 'versionId and toVersionId are both required for diff mode'
2240
2521
  };
2522
+ }
2523
+ try {
2524
+ const diff = await versioningService.compareVersions(versionId, toVersionId, input.workflowId);
2525
+ return {
2526
+ success: true,
2527
+ data: withDiffFormat(diff, 'n8n-mcp')
2528
+ };
2529
+ }
2530
+ catch (error) {
2531
+ const message = error instanceof Error ? error.message : String(error);
2532
+ if (message.includes(workflow_versioning_service_1.VERSION_OWNERSHIP_ERROR_PREFIX)) {
2533
+ return {
2534
+ success: false,
2535
+ code: 'INVALID_ARGS',
2536
+ error: message
2537
+ };
2538
+ }
2539
+ throw error;
2540
+ }
2241
2541
  }
2542
+ default:
2543
+ return {
2544
+ success: false,
2545
+ error: `Unknown mode: ${input.mode}`
2546
+ };
2547
+ }
2548
+ }
2549
+ async function handleWorkflowVersions(args, repository, context) {
2550
+ try {
2551
+ const input = workflowVersionsSchema.parse(args);
2552
+ if (process.env.ENABLE_MULTI_TENANT === 'true' && (0, instance_context_1.getInstanceScopeId)(context) === '') {
2553
+ const source = input.source ?? 'local';
2554
+ return {
2555
+ success: false,
2556
+ mode: input.mode,
2557
+ source,
2558
+ backend: source === 'native' ? 'official-mcp' : 'n8n-mcp',
2559
+ error: source === 'native'
2560
+ ? "Reading n8n's own version history needs an instance-scoped context for this tenant"
2561
+ : 'Workflow version storage is not available for this tenant context'
2562
+ };
2563
+ }
2564
+ if ((input.source ?? 'local') === 'native') {
2565
+ return handleNativeWorkflowVersions(input, context);
2566
+ }
2567
+ const invalidLocalVersionId = (error) => ({
2568
+ success: false,
2569
+ mode: input.mode,
2570
+ source: 'local',
2571
+ backend: 'n8n-mcp',
2572
+ code: 'INVALID_ARGS',
2573
+ error,
2574
+ });
2575
+ const parsedVersionId = parseLocalVersionId(input.versionId, 'versionId');
2576
+ if (!parsedVersionId.ok)
2577
+ return invalidLocalVersionId(parsedVersionId.error);
2578
+ const parsedToVersionId = parseLocalVersionId(input.toVersionId, 'toVersionId');
2579
+ if (!parsedToVersionId.ok)
2580
+ return invalidLocalVersionId(parsedToVersionId.error);
2581
+ const versionId = parsedVersionId.value;
2582
+ const toVersionId = parsedToVersionId.value;
2583
+ const localResult = await handleLocalWorkflowVersions(input, versionId, toVersionId, repository, context);
2584
+ return { ...localResult, mode: input.mode, source: 'local', backend: 'n8n-mcp' };
2242
2585
  }
2243
2586
  catch (error) {
2587
+ const raw = (args && typeof args === 'object' ? args : {});
2588
+ const source = raw.source === 'native' ? 'native' : 'local';
2589
+ const label = {
2590
+ ...(typeof raw.mode === 'string' ? { mode: raw.mode } : {}),
2591
+ source,
2592
+ backend: source === 'native' ? 'official-mcp' : 'n8n-mcp',
2593
+ };
2244
2594
  if (error instanceof zod_1.z.ZodError) {
2245
2595
  return {
2246
2596
  success: false,
2597
+ ...label,
2247
2598
  error: 'Invalid input',
2248
2599
  details: { errors: error.errors }
2249
2600
  };
2250
2601
  }
2251
2602
  return {
2252
2603
  success: false,
2604
+ ...label,
2253
2605
  error: error instanceof Error ? error.message : 'Unknown error occurred'
2254
2606
  };
2255
2607
  }
@@ -2716,6 +3068,115 @@ async function handleDeleteRows(args, context) {
2716
3068
  return handleCrudError(error);
2717
3069
  }
2718
3070
  }
3071
+ const DATATABLE_ACTION = 'manage_datatable';
3072
+ const DATATABLE_TIMEOUT_MS = agents_action_map_1.DEFAULT_TIMEOUT_MS;
3073
+ const COLUMN_TOOLS = {
3074
+ addColumn: ['add_data_table_column'],
3075
+ deleteColumn: ['delete_data_table_column'],
3076
+ renameColumn: ['rename_data_table_column'],
3077
+ };
3078
+ const columnNameSchema = zod_1.z
3079
+ .string()
3080
+ .min(1, 'Column name cannot be empty')
3081
+ .max(63, 'Column name must be at most 63 characters')
3082
+ .regex(/^[a-zA-Z][a-zA-Z0-9_]*$/, 'Column name must start with a letter and contain only letters, digits and underscores');
3083
+ const columnTargetSchema = tableIdSchema.extend({
3084
+ projectId: optionalEmptyAware(zod_1.z.string()),
3085
+ timeoutMs: zod_1.z.number().int().min(agents_action_map_1.MIN_TIMEOUT_MS).max(agents_action_map_1.MAX_TIMEOUT_MS).optional(),
3086
+ });
3087
+ const addColumnSchema = columnTargetSchema.extend({
3088
+ column: zod_1.z.preprocess(tryParseJson, zod_1.z.object({
3089
+ name: columnNameSchema,
3090
+ type: zod_1.z.enum(['string', 'number', 'boolean', 'date']),
3091
+ })),
3092
+ });
3093
+ const deleteColumnSchema = columnTargetSchema.extend({
3094
+ columnId: zod_1.z.string().min(1, 'columnId is required'),
3095
+ });
3096
+ const renameColumnSchema = deleteColumnSchema.extend({
3097
+ name: columnNameSchema,
3098
+ });
3099
+ function columnInvalidArgs(action, error) {
3100
+ return {
3101
+ success: false,
3102
+ action,
3103
+ code: 'INVALID_ARGS',
3104
+ error: error.issues.map(i => `${i.path.join('.') || 'input'}: ${i.message}`).join('; '),
3105
+ };
3106
+ }
3107
+ async function resolveDataTableProjectId(input, action, context) {
3108
+ if (input.projectId)
3109
+ return { projectId: input.projectId };
3110
+ const resolved = await (0, handlers_official_tools_1.resolveProjectChoices)(context);
3111
+ if ('failure' in resolved)
3112
+ return { failure: { ...resolved.failure, action } };
3113
+ const items = resolved.choices.items;
3114
+ if (items.length === 1)
3115
+ return { projectId: items[0].id };
3116
+ const backend = resolved.choices.backend;
3117
+ if (items.length === 0) {
3118
+ return {
3119
+ failure: {
3120
+ success: false,
3121
+ action,
3122
+ backend,
3123
+ code: 'PROJECT_REQUIRED',
3124
+ error: 'No project could be resolved for this instance; pass projectId',
3125
+ },
3126
+ };
3127
+ }
3128
+ return {
3129
+ failure: {
3130
+ success: false,
3131
+ action,
3132
+ backend,
3133
+ code: 'PROJECT_REQUIRED',
3134
+ error: 'Several projects are accessible; pass projectId',
3135
+ details: { candidates: items },
3136
+ },
3137
+ };
3138
+ }
3139
+ async function callColumnTool(action, officialArgs, timeoutMs, context) {
3140
+ const response = await (0, handlers_official_tools_1.callOfficialTool)(context, COLUMN_TOOLS[action], officialArgs, timeoutMs ?? DATATABLE_TIMEOUT_MS, DATATABLE_ACTION, false);
3141
+ return { ...response, action, backend: 'official-mcp' };
3142
+ }
3143
+ async function handleAddColumn(args, context) {
3144
+ const parsed = addColumnSchema.safeParse(args);
3145
+ if (!parsed.success)
3146
+ return columnInvalidArgs('addColumn', parsed.error);
3147
+ const resolved = await resolveDataTableProjectId(parsed.data, 'addColumn', context);
3148
+ if ('failure' in resolved)
3149
+ return resolved.failure;
3150
+ return callColumnTool('addColumn', {
3151
+ dataTableId: parsed.data.tableId,
3152
+ projectId: resolved.projectId,
3153
+ name: parsed.data.column.name,
3154
+ type: parsed.data.column.type,
3155
+ }, parsed.data.timeoutMs, context);
3156
+ }
3157
+ async function handleDeleteColumn(args, context) {
3158
+ const parsed = deleteColumnSchema.safeParse(args);
3159
+ if (!parsed.success)
3160
+ return columnInvalidArgs('deleteColumn', parsed.error);
3161
+ const resolved = await resolveDataTableProjectId(parsed.data, 'deleteColumn', context);
3162
+ if ('failure' in resolved)
3163
+ return resolved.failure;
3164
+ return callColumnTool('deleteColumn', { dataTableId: parsed.data.tableId, projectId: resolved.projectId, columnId: parsed.data.columnId }, parsed.data.timeoutMs, context);
3165
+ }
3166
+ async function handleRenameColumn(args, context) {
3167
+ const parsed = renameColumnSchema.safeParse(args);
3168
+ if (!parsed.success)
3169
+ return columnInvalidArgs('renameColumn', parsed.error);
3170
+ const resolved = await resolveDataTableProjectId(parsed.data, 'renameColumn', context);
3171
+ if ('failure' in resolved)
3172
+ return resolved.failure;
3173
+ return callColumnTool('renameColumn', {
3174
+ dataTableId: parsed.data.tableId,
3175
+ projectId: resolved.projectId,
3176
+ columnId: parsed.data.columnId,
3177
+ name: parsed.data.name,
3178
+ }, parsed.data.timeoutMs, context);
3179
+ }
2719
3180
  const listCredentialsSchema = zod_1.z.object({
2720
3181
  includeUsage: zod_1.z.boolean().optional(),
2721
3182
  cursor: optionalEmptyAware(zod_1.z.string()),