powerautomate-mcp 0.7.9 → 0.9.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.
package/dist/index.js CHANGED
@@ -1134,6 +1134,28 @@ async function createAuthProviderFromConfig(clientId, tenantId, cachePath, silen
1134
1134
  };
1135
1135
  return createWamAuthProvider(config, silentOnly);
1136
1136
  }
1137
+
1138
+ // src/utils/jwt-claims.ts
1139
+ function decodeJwtClaims(token) {
1140
+ try {
1141
+ const parts = token.split(".");
1142
+ if (parts.length < 2 || !parts[1]) return {};
1143
+ const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1144
+ const padding = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
1145
+ const json = Buffer.from(padded + padding, "base64").toString("utf-8");
1146
+ const claims = JSON.parse(json);
1147
+ return claims && typeof claims === "object" ? claims : {};
1148
+ } catch {
1149
+ return {};
1150
+ }
1151
+ }
1152
+ function getOidFromToken(token) {
1153
+ const claims = decodeJwtClaims(token);
1154
+ const oid = claims.oid;
1155
+ return typeof oid === "string" && oid.length > 0 ? oid : void 0;
1156
+ }
1157
+
1158
+ // src/api/flow-management.ts
1137
1159
  var apiLogger = createChildLogger({ module: "flow-api" });
1138
1160
  var FlowManagementApi = class {
1139
1161
  authProvider;
@@ -1142,6 +1164,18 @@ var FlowManagementApi = class {
1142
1164
  this.authProvider = config.authProvider;
1143
1165
  this.defaultEnvironmentId = config.defaultEnvironmentId;
1144
1166
  }
1167
+ /**
1168
+ * Return the current user's Azure AD object ID derived from the access token.
1169
+ * Used for client-side ownership filtering. Returns undefined if extraction fails.
1170
+ */
1171
+ async getCurrentUserOid() {
1172
+ try {
1173
+ const token = await this.authProvider.getToken();
1174
+ return getOidFromToken(token);
1175
+ } catch {
1176
+ return void 0;
1177
+ }
1178
+ }
1145
1179
  /**
1146
1180
  * Get authorization header with bearer token
1147
1181
  */
@@ -1484,6 +1518,30 @@ var FlowManagementApi = class {
1484
1518
  return data;
1485
1519
  }, "Fetch resource link");
1486
1520
  }
1521
+ async getFlowRunActionRepetitions(flowId, runId, actionName, environmentId) {
1522
+ const envId = environmentId ?? this.defaultEnvironmentId;
1523
+ const safeFlowId = validateRowId(flowId);
1524
+ const safeRunId = validateFlowRunId(runId);
1525
+ const safeActionName = encodeURIComponent(actionName);
1526
+ apiLogger.debug({ flowId: safeFlowId, runId: safeRunId, actionName }, "Getting action repetitions");
1527
+ const url = this.buildUrl(
1528
+ envId,
1529
+ `/flows/${safeFlowId}/runs/${safeRunId}/actions/${safeActionName}/repetitions`
1530
+ );
1531
+ const headers = await this.getAuthHeaders();
1532
+ return wrapApiCall(async () => {
1533
+ const response = await request(url, { method: "GET", headers });
1534
+ const data = await this.handleResponse(
1535
+ response,
1536
+ `Get repetitions for action ${actionName}`
1537
+ );
1538
+ apiLogger.info(
1539
+ { flowId, runId, actionName, count: data.value.length },
1540
+ "Retrieved action repetitions"
1541
+ );
1542
+ return data.value;
1543
+ }, `Get repetitions for action ${actionName}`);
1544
+ }
1487
1545
  // ==========================================================================
1488
1546
  // Flow Sharing & Permissions
1489
1547
  // ==========================================================================
@@ -4212,11 +4270,15 @@ var listFlowsInputSchema = z.object({
4212
4270
  environment: z.string().optional().describe("Environment name or GUID. Uses default if omitted."),
4213
4271
  search: z.string().optional().describe("Search flows by display name (case-insensitive substring match). Applied client-side after fetching."),
4214
4272
  filter: z.string().optional().describe(`OData filter expression (e.g., "properties/state eq 'Started'")`),
4215
- top: z.number().min(1).max(250).optional().describe("Maximum number of results to return (default 50, max 250)")
4273
+ top: z.number().min(1).max(250).optional().describe("Maximum number of results to return (default 50, max 250)"),
4274
+ scope: z.enum(["owned", "shared", "all"]).optional().default("all").describe(
4275
+ "Filter by ownership: 'owned' = only flows you created, 'shared' = only flows shared with you, 'all' = both (default)."
4276
+ ),
4277
+ includeOwner: z.boolean().optional().default(true).describe("Include owner/creator name in the output (default true).")
4216
4278
  });
4217
4279
  var listFlowsTool = {
4218
4280
  name: "list_flows",
4219
- description: "List Power Automate flows in an environment. Returns flow names, IDs, state (Started/Stopped), and last modified dates. Use this to discover what flows exist before getting details or making changes.",
4281
+ description: "List Power Automate flows in an environment. Returns flow names, IDs, state (Started/Stopped), last modified dates, and owners. Use scope='shared' to find flows shared with you, scope='owned' for your flows only. Use this to discover what flows exist before getting details or making changes.",
4220
4282
  inputSchema: {
4221
4283
  type: "object",
4222
4284
  properties: {
@@ -4237,6 +4299,17 @@ var listFlowsTool = {
4237
4299
  description: "Maximum number of results to return (default 50, max 250)",
4238
4300
  minimum: 1,
4239
4301
  maximum: 250
4302
+ },
4303
+ scope: {
4304
+ type: "string",
4305
+ enum: ["owned", "shared", "all"],
4306
+ description: "Filter by ownership: 'owned' = only flows you created, 'shared' = only flows shared with you, 'all' = both (default).",
4307
+ default: "all"
4308
+ },
4309
+ includeOwner: {
4310
+ type: "boolean",
4311
+ description: "Include owner/creator name in the output (default true).",
4312
+ default: true
4240
4313
  }
4241
4314
  }
4242
4315
  }
@@ -4262,14 +4335,39 @@ async function handleListFlows(api, input) {
4262
4335
  const needle = parsed.search.toLowerCase();
4263
4336
  flows = flows.filter((f) => f.properties.displayName.toLowerCase().includes(needle));
4264
4337
  }
4338
+ let currentUserOid;
4339
+ if (parsed.scope !== "all" || parsed.includeOwner) {
4340
+ currentUserOid = await api.getCurrentUserOid();
4341
+ if (parsed.scope !== "all" && !currentUserOid) {
4342
+ logger.warn("list_flows: could not derive current user OID; scope filter will not apply");
4343
+ }
4344
+ }
4345
+ if (parsed.scope !== "all" && currentUserOid) {
4346
+ flows = flows.filter((f) => {
4347
+ const creatorId = f.properties.creator?.id;
4348
+ if (!creatorId) return false;
4349
+ const isOwned = creatorId === currentUserOid;
4350
+ return parsed.scope === "owned" ? isOwned : !isOwned;
4351
+ });
4352
+ }
4265
4353
  if (flows.length === 0) {
4266
- return "No flows found";
4354
+ return parsed.scope === "shared" ? "No flows shared with you in this environment." : parsed.scope === "owned" ? "No flows you own in this environment." : "No flows found";
4267
4355
  }
4268
- const lines = [`Flows (${flows.length}):`];
4356
+ const scopeLabel = parsed.scope === "owned" ? "Owned" : parsed.scope === "shared" ? "Shared with you" : "Flows";
4357
+ const lines = [`${scopeLabel} (${flows.length}):`];
4269
4358
  for (const flow of flows) {
4270
4359
  const state = flow.properties.state === "Started" ? "\u25B6" : "\u23F9";
4271
4360
  const modified = new Date(flow.properties.lastModifiedTime).toLocaleDateString();
4272
- lines.push(` ${state} ${flow.properties.displayName} (${flow.name}) - ${modified}`);
4361
+ let line = ` ${state} ${flow.properties.displayName} (${flow.name}) - ${modified}`;
4362
+ if (parsed.includeOwner) {
4363
+ const creator = flow.properties.creator;
4364
+ const ownerName = creator?.displayName ?? creator?.email;
4365
+ const isOwnedTag = currentUserOid && creator?.id === currentUserOid ? " [owned]" : creator ? " [shared]" : "";
4366
+ if (ownerName) {
4367
+ line += ` \u2014 ${ownerName}${isOwnedTag}`;
4368
+ }
4369
+ }
4370
+ lines.push(line);
4273
4371
  }
4274
4372
  if (result.nextLink) {
4275
4373
  lines.push(`
@@ -4280,11 +4378,14 @@ async function handleListFlows(api, input) {
4280
4378
  var getFlowInputSchema = z.object({
4281
4379
  flowId: z.string().describe("The flow ID (GUID) to retrieve"),
4282
4380
  environment: z.string().optional().describe("Environment name or GUID. Uses default if omitted."),
4283
- includeDefinition: z.boolean().optional().default(true).describe("Include the full flow definition JSON (default true)")
4381
+ includeDefinition: z.boolean().optional().default(true).describe("Include the full flow definition JSON (default true)"),
4382
+ format: z.enum(["summary", "json", "both"]).optional().default("summary").describe(
4383
+ "Output format: 'summary' = human-readable top-level overview (default), 'json' = full raw flow.properties.definition with all nested actions, 'both' = summary followed by full JSON. Use 'json' or 'both' to capture nested actions inside Switch/If/Foreach/Scope."
4384
+ )
4284
4385
  });
4285
4386
  var getFlowTool = {
4286
4387
  name: "get_flow",
4287
- description: "Get the complete definition of a Power Automate flow including triggers, actions, and connection references. Use list_flows first to find flow IDs.",
4388
+ description: "Get the complete definition of a Power Automate flow including triggers, actions, connection references, and description. Use list_flows first to find flow IDs. Set format='json' or format='both' to capture the FULL definition including nested actions inside Switch/If/Foreach/Scope (the default 'summary' format only shows top-level actions).",
4288
4389
  inputSchema: {
4289
4390
  type: "object",
4290
4391
  properties: {
@@ -4300,6 +4401,12 @@ var getFlowTool = {
4300
4401
  type: "boolean",
4301
4402
  description: "Include the full flow definition JSON (default true)",
4302
4403
  default: true
4404
+ },
4405
+ format: {
4406
+ type: "string",
4407
+ enum: ["summary", "json", "both"],
4408
+ description: "Output format: 'summary' = top-level overview (default), 'json' = full raw definition with all nested actions, 'both' = summary plus full JSON.",
4409
+ default: "summary"
4303
4410
  }
4304
4411
  },
4305
4412
  required: ["flowId"]
@@ -4307,7 +4414,7 @@ var getFlowTool = {
4307
4414
  };
4308
4415
  async function handleGetFlow(api, input) {
4309
4416
  const parsed = getFlowInputSchema.parse(input);
4310
- logger.info({ flowId: parsed.flowId }, "Executing get_flow");
4417
+ logger.info({ flowId: parsed.flowId, format: parsed.format }, "Executing get_flow");
4311
4418
  const flow = await api.getFlow(parsed.flowId, parsed.environment);
4312
4419
  const triggers = flow.properties.definition.triggers;
4313
4420
  const actions = flow.properties.definition.actions;
@@ -4320,6 +4427,22 @@ async function handleGetFlow(api, input) {
4320
4427
  tier: ref.tier
4321
4428
  })
4322
4429
  );
4430
+ const description = flow.properties.description;
4431
+ const creator = flow.properties.creator;
4432
+ if (parsed.format === "json") {
4433
+ const payload = {
4434
+ id: flow.name,
4435
+ displayName: flow.properties.displayName,
4436
+ description,
4437
+ state: flow.properties.state,
4438
+ createdTime: flow.properties.createdTime,
4439
+ lastModifiedTime: flow.properties.lastModifiedTime,
4440
+ creator,
4441
+ connectionReferences: flow.properties.connectionReferences,
4442
+ definition: flow.properties.definition
4443
+ };
4444
+ return JSON.stringify(payload, null, 2);
4445
+ }
4323
4446
  const lines = [
4324
4447
  `## ${flow.properties.displayName}`,
4325
4448
  ``,
@@ -4329,6 +4452,12 @@ async function handleGetFlow(api, input) {
4329
4452
  `**Actions:** ${Object.keys(actions).length}`,
4330
4453
  `**Modified:** ${new Date(flow.properties.lastModifiedTime).toLocaleDateString()}`
4331
4454
  ];
4455
+ if (description) {
4456
+ lines.push(`**Description:** ${description}`);
4457
+ }
4458
+ if (creator?.displayName || creator?.email) {
4459
+ lines.push(`**Owner:** ${creator.displayName ?? creator.email}${creator.id ? ` (${creator.id})` : ""}`);
4460
+ }
4332
4461
  if (connectionRefs.length > 0) {
4333
4462
  lines.push(``, `### Connections`);
4334
4463
  for (const conn of connectionRefs) {
@@ -4337,7 +4466,7 @@ async function handleGetFlow(api, input) {
4337
4466
  }
4338
4467
  }
4339
4468
  if (parsed.includeDefinition !== false) {
4340
- lines.push(``, `### Actions`);
4469
+ lines.push(``, `### Actions (top-level)`);
4341
4470
  for (const [name, action] of Object.entries(actions)) {
4342
4471
  const act = action;
4343
4472
  const deps = act.runAfter ? Object.keys(act.runAfter).join(", ") : "trigger";
@@ -4346,6 +4475,11 @@ async function handleGetFlow(api, input) {
4346
4475
  lines.push(` inputs: ${JSON.stringify(act.inputs, null, 2).split("\n").join("\n ")}`);
4347
4476
  }
4348
4477
  }
4478
+ lines.push(``);
4479
+ lines.push(`_Note: Nested actions inside Switch/If/Foreach/Scope are not expanded in this view. Call get_flow with format="json" or format="both" to capture them._`);
4480
+ }
4481
+ if (parsed.format === "both") {
4482
+ lines.push(``, `### Full Definition (JSON)`, "```json", JSON.stringify(flow.properties.definition, null, 2), "```");
4349
4483
  }
4350
4484
  return lines.join("\n");
4351
4485
  }
@@ -5802,6 +5936,7 @@ var connectionRefSchema = z.object({
5802
5936
  });
5803
5937
  var createFlowInputSchema = z.object({
5804
5938
  displayName: z.string().describe("The display name for the new flow"),
5939
+ description: z.string().optional().describe("Optional description for the flow"),
5805
5940
  trigger: triggerSchema.describe("The trigger definition"),
5806
5941
  triggerName: z.string().optional().default("manual").describe("Name for the trigger (default: 'manual')"),
5807
5942
  actions: z.record(actionSchema).describe("Actions in the flow, keyed by action name"),
@@ -5819,6 +5954,10 @@ var createFlowTool = {
5819
5954
  type: "string",
5820
5955
  description: "The display name for the new flow"
5821
5956
  },
5957
+ description: {
5958
+ type: "string",
5959
+ description: "Optional description shown in the Power Automate UI"
5960
+ },
5822
5961
  trigger: {
5823
5962
  type: "object",
5824
5963
  description: "The trigger definition with type, kind, inputs, and optional recurrence",
@@ -5946,7 +6085,8 @@ async function handleCreateFlow(api, input) {
5946
6085
  properties: {
5947
6086
  displayName: parsed.displayName,
5948
6087
  definition,
5949
- connectionReferences: Object.keys(connectionReferences).length > 0 ? connectionReferences : void 0
6088
+ connectionReferences: Object.keys(connectionReferences).length > 0 ? connectionReferences : void 0,
6089
+ ...parsed.description !== void 0 ? { description: parsed.description } : {}
5950
6090
  }
5951
6091
  };
5952
6092
  try {
@@ -6008,16 +6148,25 @@ var connectionRefSchema2 = z.object({
6008
6148
  var updateFlowInputSchema = z.object({
6009
6149
  flowId: z.string().describe("The flow ID (GUID) to update"),
6010
6150
  displayName: z.string().optional().describe("New display name for the flow"),
6151
+ description: z.string().optional().describe("New description (set to empty string to clear)"),
6011
6152
  trigger: triggerSchema2.optional().describe("New trigger definition (replaces existing)"),
6012
6153
  triggerName: z.string().optional().describe("Name for the trigger"),
6013
- actions: z.record(actionSchema2).optional().describe("New actions (replaces all existing actions)"),
6154
+ actions: z.record(actionSchema2).optional().describe(
6155
+ "Actions. By default REPLACES all existing actions (you must include every action you want kept). When mergeActions=true, only the named actions are added/updated; everything else stays. Use patchActions for surgical edits to a single nested branch (smallest payload)."
6156
+ ),
6157
+ mergeActions: z.boolean().optional().default(false).describe(
6158
+ "When true, deep-merge the provided 'actions' onto the current flow's actions instead of replacing wholesale. Recurses into nested actions/cases/else/foreach. Lets you patch a sub-tree without resending the entire flow."
6159
+ ),
6160
+ patchActions: z.record(actionSchema2.nullable()).optional().describe(
6161
+ "Path-based patch map. Keys are slash-separated paths into the action tree, e.g. 'If_Recognized_Form/cases/Default/actions/Compose'. Set value to null to delete a node. Smallest payload \u2014 only the changed branches travel. Applied AFTER mergeActions."
6162
+ ),
6014
6163
  connectionReferences: z.record(connectionRefSchema2).optional().describe("New connection references (replaces existing)"),
6015
6164
  environment: z.string().optional().describe("Environment name or GUID"),
6016
6165
  validateBeforeUpdate: z.boolean().optional().default(true).describe("Validate before updating")
6017
6166
  });
6018
6167
  var updateFlowTool = {
6019
6168
  name: "update_flow",
6020
- description: "Update an existing Power Automate flow. You can update the display name, trigger, actions, or connection references. Use get_flow first to see the current definition. Note: Updating replaces the entire definition, so include all actions you want to keep.",
6169
+ description: "Update an existing Power Automate flow. Three update modes: (1) full replace \u2014 pass 'actions' to replace all top-level actions (default; you must include nested actions you want kept), (2) merge \u2014 set mergeActions=true to deep-merge only the actions you provide, leaving the rest intact, (3) patch \u2014 use patchActions with path keys (e.g. 'If/cases/Default/actions/Compose') for surgical edits with the smallest payload. Use get_flow with format='json' first to see the full nested action tree.",
6021
6170
  inputSchema: {
6022
6171
  type: "object",
6023
6172
  properties: {
@@ -6029,6 +6178,10 @@ var updateFlowTool = {
6029
6178
  type: "string",
6030
6179
  description: "New display name for the flow"
6031
6180
  },
6181
+ description: {
6182
+ type: "string",
6183
+ description: "New description for the flow (empty string clears)"
6184
+ },
6032
6185
  trigger: {
6033
6186
  type: "object",
6034
6187
  description: "New trigger definition (replaces existing)"
@@ -6039,7 +6192,16 @@ var updateFlowTool = {
6039
6192
  },
6040
6193
  actions: {
6041
6194
  type: "object",
6042
- description: "New actions (replaces all existing actions)"
6195
+ description: "Actions. By default replaces all existing top-level actions. With mergeActions=true, deep-merges instead."
6196
+ },
6197
+ mergeActions: {
6198
+ type: "boolean",
6199
+ description: "When true, deep-merge 'actions' onto current flow instead of replacing. Recurses into nested actions/cases/else/foreach.",
6200
+ default: false
6201
+ },
6202
+ patchActions: {
6203
+ type: "object",
6204
+ description: "Path-based patch map. Keys: slash-separated paths like 'If/cases/Default/actions/Compose'. Value=null deletes the node. Smallest payload."
6043
6205
  },
6044
6206
  connectionReferences: {
6045
6207
  type: "object",
@@ -6057,17 +6219,80 @@ var updateFlowTool = {
6057
6219
  required: ["flowId"]
6058
6220
  }
6059
6221
  };
6222
+ function deepMerge(base, patch) {
6223
+ const out = { ...base };
6224
+ for (const [key, value] of Object.entries(patch)) {
6225
+ if (value === void 0) continue;
6226
+ const baseVal = out[key];
6227
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
6228
+ out[key] = deepMerge(
6229
+ baseVal,
6230
+ value
6231
+ );
6232
+ } else {
6233
+ out[key] = value;
6234
+ }
6235
+ }
6236
+ return out;
6237
+ }
6238
+ function applyPathPatch(actions, path, value) {
6239
+ const segments = path.split("/").filter(Boolean);
6240
+ if (segments.length === 0) return actions;
6241
+ const result = JSON.parse(JSON.stringify(actions));
6242
+ let cursor = result;
6243
+ for (let i = 0; i < segments.length - 1; i++) {
6244
+ const seg = segments[i];
6245
+ const next = cursor[seg];
6246
+ if (next === null || typeof next !== "object" || Array.isArray(next)) {
6247
+ cursor[seg] = {};
6248
+ }
6249
+ cursor = cursor[seg];
6250
+ }
6251
+ const lastSeg = segments[segments.length - 1];
6252
+ if (value === null) {
6253
+ delete cursor[lastSeg];
6254
+ } else {
6255
+ cursor[lastSeg] = value;
6256
+ }
6257
+ return result;
6258
+ }
6060
6259
  async function handleUpdateFlow(api, input) {
6061
6260
  const parsed = updateFlowInputSchema.parse(input);
6062
- logger.info({ flowId: parsed.flowId }, "Executing update_flow");
6261
+ logger.info(
6262
+ {
6263
+ flowId: parsed.flowId,
6264
+ mergeActions: parsed.mergeActions,
6265
+ hasPatch: !!parsed.patchActions
6266
+ },
6267
+ "Executing update_flow"
6268
+ );
6063
6269
  const currentFlow = await api.getFlow(parsed.flowId, parsed.environment);
6064
6270
  const currentDef = currentFlow.properties.definition;
6065
6271
  const currentConnRefs = currentFlow.properties.connectionReferences;
6066
6272
  const newDisplayName = parsed.displayName ?? currentFlow.properties.displayName;
6273
+ const newDescription = parsed.description !== void 0 ? parsed.description : currentFlow.properties.description;
6067
6274
  let newDefinition;
6068
- if (parsed.trigger || parsed.actions) {
6275
+ let resolvedActions;
6276
+ if (parsed.actions || parsed.patchActions) {
6277
+ if (parsed.mergeActions && parsed.actions) {
6278
+ resolvedActions = deepMerge(
6279
+ currentDef.actions,
6280
+ parsed.actions
6281
+ );
6282
+ } else if (parsed.actions) {
6283
+ resolvedActions = parsed.actions;
6284
+ } else {
6285
+ resolvedActions = JSON.parse(JSON.stringify(currentDef.actions));
6286
+ }
6287
+ if (parsed.patchActions) {
6288
+ for (const [path, value] of Object.entries(parsed.patchActions)) {
6289
+ resolvedActions = applyPathPatch(resolvedActions, path, value);
6290
+ }
6291
+ }
6292
+ }
6293
+ if (parsed.trigger || resolvedActions) {
6069
6294
  const triggerName = parsed.triggerName ?? Object.keys(currentDef.triggers)[0] ?? "manual";
6070
- const processedActions = parsed.actions ? preprocessFlowActions(parsed.actions) : currentDef.actions;
6295
+ const processedActions = resolvedActions ? preprocessFlowActions(resolvedActions) : currentDef.actions;
6071
6296
  newDefinition = {
6072
6297
  $schema: currentDef.$schema,
6073
6298
  contentVersion: currentDef.contentVersion,
@@ -6121,7 +6346,8 @@ async function handleUpdateFlow(api, input) {
6121
6346
  properties: {
6122
6347
  displayName: newDisplayName,
6123
6348
  definition: newDefinition,
6124
- connectionReferences: newConnRefs
6349
+ connectionReferences: newConnRefs,
6350
+ ...newDescription !== void 0 ? { description: newDescription } : {}
6125
6351
  }
6126
6352
  };
6127
6353
  try {
@@ -6670,11 +6896,14 @@ var getRunActionsInputSchema = z.object({
6670
6896
  flowId: z.string().describe("The flow ID (GUID)"),
6671
6897
  runId: z.string().describe("The run ID (GUID)"),
6672
6898
  environment: z.string().optional().describe("Environment name or GUID. Uses default if omitted."),
6673
- failedOnly: z.boolean().optional().describe("If true, only return failed actions (default: false)")
6899
+ failedOnly: z.boolean().optional().describe("If true, only return failed actions (default: false)"),
6900
+ includeInputs: z.boolean().optional().describe("If true, fetch and display input data for each action (default: false for succeeded, true for failed)"),
6901
+ includeOutputs: z.boolean().optional().describe("If true, fetch and display output data for each action (default: false for succeeded, true for failed)"),
6902
+ actionName: z.string().optional().describe("Filter to a specific action by name (partial match). Use to drill into one action's details.")
6674
6903
  });
6675
6904
  var getRunActionsTool = {
6676
6905
  name: "get_run_actions",
6677
- description: "Get detailed action-level information for a flow run. Shows each action's status, timing, and most importantly - error details for failed actions. Essential for debugging why a flow failed. Use 'failedOnly: true' to focus on just the failures.",
6906
+ description: "Get detailed action-level information for a flow run. Shows each action's status, timing, inputs/outputs, and error details. Use 'failedOnly: true' to focus on failures, 'actionName' to drill into a specific action, 'includeInputs'/'includeOutputs' to see full data payloads.",
6678
6907
  inputSchema: {
6679
6908
  type: "object",
6680
6909
  properties: {
@@ -6693,11 +6922,34 @@ var getRunActionsTool = {
6693
6922
  failedOnly: {
6694
6923
  type: "boolean",
6695
6924
  description: "If true, only return failed actions (default: false)"
6925
+ },
6926
+ includeInputs: {
6927
+ type: "boolean",
6928
+ description: "If true, fetch and display input data for each action (default: false for succeeded, true for failed)"
6929
+ },
6930
+ includeOutputs: {
6931
+ type: "boolean",
6932
+ description: "If true, fetch and display output data for each action (default: false for succeeded, true for failed)"
6933
+ },
6934
+ actionName: {
6935
+ type: "string",
6936
+ description: "Filter to a specific action by name (partial match). Use to drill into one action's details."
6696
6937
  }
6697
6938
  },
6698
6939
  required: ["flowId", "runId"]
6699
6940
  }
6700
6941
  };
6942
+ var TRUNCATE_LIMIT = 1e3;
6943
+ var ERROR_TRUNCATE_LIMIT = 500;
6944
+ function formatResourceData(data) {
6945
+ if (data && typeof data === "object" && "body" in data) {
6946
+ const body = data.body;
6947
+ const str2 = typeof body === "object" ? JSON.stringify(body, null, 2) : String(body);
6948
+ return str2.length > TRUNCATE_LIMIT ? str2.slice(0, TRUNCATE_LIMIT) + "\u2026" : str2;
6949
+ }
6950
+ const str = JSON.stringify(data, null, 2);
6951
+ return str.length > TRUNCATE_LIMIT ? str.slice(0, TRUNCATE_LIMIT) + "\u2026" : str;
6952
+ }
6701
6953
  async function handleGetRunActions(api, input) {
6702
6954
  const parsed = getRunActionsInputSchema.parse(input);
6703
6955
  logger.info({ input: parsed }, "Executing get_run_actions");
@@ -6707,38 +6959,70 @@ async function handleGetRunActions(api, input) {
6707
6959
  parsed.environment
6708
6960
  );
6709
6961
  let filteredActions = actions;
6962
+ if (parsed.actionName) {
6963
+ const needle = parsed.actionName.toLowerCase();
6964
+ filteredActions = filteredActions.filter(
6965
+ (a) => a.name.toLowerCase().includes(needle)
6966
+ );
6967
+ }
6710
6968
  if (parsed.failedOnly) {
6711
- filteredActions = actions.filter(
6969
+ filteredActions = filteredActions.filter(
6712
6970
  (a) => a.properties.status === "Failed" || a.properties.status === "TimedOut"
6713
6971
  );
6714
6972
  }
6715
- const keyActionNames = ["Result", "Debug_Sample", "Debug_F1", "Final_Count"];
6716
- const outputsToFetch = [];
6973
+ const shouldFetchInputs = (status) => {
6974
+ if (parsed.includeInputs === true) return true;
6975
+ if (parsed.includeInputs === false) return false;
6976
+ return status === "Failed" || status === "TimedOut";
6977
+ };
6978
+ const shouldFetchOutputs = (status) => {
6979
+ if (parsed.includeOutputs === true) return true;
6980
+ if (parsed.includeOutputs === false) return false;
6981
+ return status === "Failed" || status === "TimedOut";
6982
+ };
6983
+ const actionIO = {};
6984
+ const fetchPromises = [];
6717
6985
  for (const action of filteredActions) {
6718
- const isKeyAction = keyActionNames.some((k) => action.name.includes(k) || action.name === k);
6719
- if (isKeyAction && action.properties.outputsLink?.uri) {
6720
- outputsToFetch.push({ name: action.name, uri: action.properties.outputsLink.uri });
6986
+ const status = action.properties.status;
6987
+ const needInputs = shouldFetchInputs(status) && action.properties.inputsLink?.uri;
6988
+ const needOutputs = shouldFetchOutputs(status) && action.properties.outputsLink?.uri;
6989
+ if (needInputs) {
6990
+ fetchPromises.push(
6991
+ api.fetchResourceLink(action.properties.inputsLink.uri).then(
6992
+ (data) => {
6993
+ if (!actionIO[action.name]) actionIO[action.name] = {};
6994
+ actionIO[action.name].inputs = formatResourceData(data);
6995
+ },
6996
+ (err) => {
6997
+ if (!actionIO[action.name]) actionIO[action.name] = {};
6998
+ const msg = err instanceof Error ? err.message : String(err);
6999
+ actionIO[action.name].inputs = `(fetch error: ${msg.slice(0, 100)})`;
7000
+ }
7001
+ )
7002
+ );
6721
7003
  }
6722
- }
6723
- const actionOutputs = {};
6724
- for (const { name, uri } of outputsToFetch) {
6725
- try {
6726
- const output = await api.fetchResourceLink(uri);
6727
- if (output && typeof output === "object" && "body" in output) {
6728
- const body = output.body;
6729
- actionOutputs[name] = typeof body === "object" ? JSON.stringify(body).slice(0, 200) : String(body);
6730
- } else {
6731
- actionOutputs[name] = JSON.stringify(output).slice(0, 200);
6732
- }
6733
- } catch (err) {
6734
- const msg = err instanceof Error ? err.message : String(err);
6735
- actionOutputs[name] = `(error: ${msg.slice(0, 50)})`;
7004
+ if (needOutputs) {
7005
+ fetchPromises.push(
7006
+ api.fetchResourceLink(action.properties.outputsLink.uri).then(
7007
+ (data) => {
7008
+ if (!actionIO[action.name]) actionIO[action.name] = {};
7009
+ actionIO[action.name].outputs = formatResourceData(data);
7010
+ },
7011
+ (err) => {
7012
+ if (!actionIO[action.name]) actionIO[action.name] = {};
7013
+ const msg = err instanceof Error ? err.message : String(err);
7014
+ actionIO[action.name].outputs = `(fetch error: ${msg.slice(0, 100)})`;
7015
+ }
7016
+ )
7017
+ );
6736
7018
  }
6737
7019
  }
6738
- const result = filteredActions.map((action) => {
7020
+ await Promise.all(fetchPromises);
7021
+ const results = filteredActions.map((action) => {
6739
7022
  const props = action.properties;
6740
7023
  const startTime = new Date(props.startTime);
6741
7024
  const endTime = props.endTime ? new Date(props.endTime) : void 0;
7025
+ const io = actionIO[action.name];
6742
7026
  return {
6743
7027
  name: action.name,
6744
7028
  status: props.status,
@@ -6746,25 +7030,213 @@ async function handleGetRunActions(api, input) {
6746
7030
  endTime: props.endTime,
6747
7031
  durationMs: endTime ? endTime.getTime() - startTime.getTime() : void 0,
6748
7032
  error: props.error,
6749
- retryCount: props.retryHistory?.length
7033
+ inputs: io?.inputs,
7034
+ outputs: io?.outputs,
7035
+ retryCount: props.retryHistory?.length,
7036
+ trackedProperties: props.trackedProperties
6750
7037
  };
6751
7038
  });
6752
- const failedCount = result.filter((r) => r.status === "Failed" || r.status === "TimedOut").length;
6753
- const succeededCount = result.filter((r) => r.status === "Succeeded").length;
6754
- const lines = [`Actions: ${result.length} total (${succeededCount} succeeded, ${failedCount} failed)`];
6755
- if (Object.keys(actionOutputs).length > 0) {
6756
- lines.push("\nOutputs:");
6757
- for (const [name, value] of Object.entries(actionOutputs)) {
6758
- lines.push(` ${name}: ${value}`);
6759
- }
6760
- lines.push("");
7039
+ const failedCount = results.filter(
7040
+ (r) => r.status === "Failed" || r.status === "TimedOut"
7041
+ ).length;
7042
+ const succeededCount = results.filter(
7043
+ (r) => r.status === "Succeeded"
7044
+ ).length;
7045
+ const lines = [
7046
+ `Actions: ${results.length} total (${succeededCount} succeeded, ${failedCount} failed)`
7047
+ ];
7048
+ if (parsed.actionName) {
7049
+ lines[0] += ` [filtered by "${parsed.actionName}"]`;
6761
7050
  }
6762
- for (const action of result) {
7051
+ for (const action of results) {
6763
7052
  const duration = action.durationMs ? `${(action.durationMs / 1e3).toFixed(1)}s` : "";
6764
- const status = action.status === "Succeeded" ? "\u2713" : action.status === "Failed" ? "\u2717" : "\u25CB";
6765
- let line = ` ${status} ${action.name} ${duration}`;
7053
+ const status = action.status === "Succeeded" ? "\u2713" : action.status === "Failed" ? "\u2717" : action.status === "TimedOut" ? "\u23F1" : "\u25CB";
7054
+ let line = `
7055
+ ${status} ${action.name} ${duration}`;
6766
7056
  if (action.error) {
6767
- line += ` - ${action.error.message.slice(0, 100)}`;
7057
+ line += `
7058
+ Error: [${action.error.code}] ${action.error.message.slice(0, ERROR_TRUNCATE_LIMIT)}`;
7059
+ }
7060
+ if (action.inputs) {
7061
+ line += `
7062
+ Inputs: ${action.inputs}`;
7063
+ }
7064
+ if (action.outputs) {
7065
+ line += `
7066
+ Outputs: ${action.outputs}`;
7067
+ }
7068
+ if (action.trackedProperties && Object.keys(action.trackedProperties).length > 0) {
7069
+ line += `
7070
+ Tracked: ${JSON.stringify(action.trackedProperties)}`;
7071
+ }
7072
+ if (action.retryCount && action.retryCount > 0) {
7073
+ line += `
7074
+ Retries: ${action.retryCount}`;
7075
+ }
7076
+ lines.push(line);
7077
+ }
7078
+ return lines.join("\n");
7079
+ }
7080
+ var getRunActionRepetitionsInputSchema = z.object({
7081
+ flowId: z.string().describe("The flow ID (GUID)"),
7082
+ runId: z.string().describe("The run ID (GUID)"),
7083
+ actionName: z.string().describe("The exact action name inside the for_each/do_until loop"),
7084
+ environment: z.string().optional().describe("Environment name or GUID. Uses default if omitted."),
7085
+ failedOnly: z.boolean().optional().describe("If true, only return failed iterations (default: false)"),
7086
+ includeInputs: z.boolean().optional().describe("If true, fetch input data for each iteration (default: false)"),
7087
+ includeOutputs: z.boolean().optional().describe("If true, fetch output data for each iteration (default: true for failed)"),
7088
+ maxIterations: z.number().optional().describe("Maximum iterations to return (default: 50). Use to limit large loops.")
7089
+ });
7090
+ var getRunActionRepetitionsTool = {
7091
+ name: "get_run_action_repetitions",
7092
+ description: "Get iteration-level details for a for_each or do_until loop action in a flow run. Shows which iterations succeeded/failed, their inputs/outputs, and error details. Essential for debugging loops that partially fail.",
7093
+ inputSchema: {
7094
+ type: "object",
7095
+ properties: {
7096
+ flowId: {
7097
+ type: "string",
7098
+ description: "The flow ID (GUID)"
7099
+ },
7100
+ runId: {
7101
+ type: "string",
7102
+ description: "The run ID (GUID)"
7103
+ },
7104
+ actionName: {
7105
+ type: "string",
7106
+ description: "The exact action name inside the for_each/do_until loop"
7107
+ },
7108
+ environment: {
7109
+ type: "string",
7110
+ description: "Environment name or GUID. Uses default if omitted."
7111
+ },
7112
+ failedOnly: {
7113
+ type: "boolean",
7114
+ description: "If true, only return failed iterations (default: false)"
7115
+ },
7116
+ includeInputs: {
7117
+ type: "boolean",
7118
+ description: "If true, fetch input data for each iteration (default: false)"
7119
+ },
7120
+ includeOutputs: {
7121
+ type: "boolean",
7122
+ description: "If true, fetch output data for each iteration (default: true for failed)"
7123
+ },
7124
+ maxIterations: {
7125
+ type: "number",
7126
+ description: "Maximum iterations to return (default: 50). Use to limit large loops."
7127
+ }
7128
+ },
7129
+ required: ["flowId", "runId", "actionName"]
7130
+ }
7131
+ };
7132
+ var TRUNCATE_LIMIT2 = 1e3;
7133
+ function formatData(data) {
7134
+ if (data && typeof data === "object" && "body" in data) {
7135
+ const body = data.body;
7136
+ const str2 = typeof body === "object" ? JSON.stringify(body, null, 2) : String(body);
7137
+ return str2.length > TRUNCATE_LIMIT2 ? str2.slice(0, TRUNCATE_LIMIT2) + "\u2026" : str2;
7138
+ }
7139
+ const str = JSON.stringify(data, null, 2);
7140
+ return str.length > TRUNCATE_LIMIT2 ? str.slice(0, TRUNCATE_LIMIT2) + "\u2026" : str;
7141
+ }
7142
+ async function handleGetRunActionRepetitions(api, input) {
7143
+ const parsed = getRunActionRepetitionsInputSchema.parse(input);
7144
+ const maxIter = parsed.maxIterations ?? 50;
7145
+ logger.info({ input: parsed }, "Executing get_run_action_repetitions");
7146
+ const repetitions = await api.getFlowRunActionRepetitions(
7147
+ parsed.flowId,
7148
+ parsed.runId,
7149
+ parsed.actionName,
7150
+ parsed.environment
7151
+ );
7152
+ let filtered = repetitions;
7153
+ if (parsed.failedOnly) {
7154
+ filtered = filtered.filter(
7155
+ (r) => r.properties.status === "Failed" || r.properties.status === "TimedOut"
7156
+ );
7157
+ }
7158
+ const totalCount = filtered.length;
7159
+ filtered = filtered.slice(0, maxIter);
7160
+ const shouldFetchInputs = (status) => {
7161
+ if (parsed.includeInputs === true) return true;
7162
+ return false;
7163
+ };
7164
+ const shouldFetchOutputs = (status) => {
7165
+ if (parsed.includeOutputs === true) return true;
7166
+ if (parsed.includeOutputs === false) return false;
7167
+ return status === "Failed" || status === "TimedOut";
7168
+ };
7169
+ const iterIO = {};
7170
+ const fetchPromises = [];
7171
+ for (const rep of filtered) {
7172
+ const status = rep.properties.status;
7173
+ const key = rep.name;
7174
+ if (shouldFetchInputs() && rep.properties.inputsLink?.uri) {
7175
+ fetchPromises.push(
7176
+ api.fetchResourceLink(rep.properties.inputsLink.uri).then(
7177
+ (data) => {
7178
+ if (!iterIO[key]) iterIO[key] = {};
7179
+ iterIO[key].inputs = formatData(data);
7180
+ },
7181
+ (err) => {
7182
+ if (!iterIO[key]) iterIO[key] = {};
7183
+ const msg = err instanceof Error ? err.message : String(err);
7184
+ iterIO[key].inputs = `(fetch error: ${msg.slice(0, 100)})`;
7185
+ }
7186
+ )
7187
+ );
7188
+ }
7189
+ if (shouldFetchOutputs(status) && rep.properties.outputsLink?.uri) {
7190
+ fetchPromises.push(
7191
+ api.fetchResourceLink(rep.properties.outputsLink.uri).then(
7192
+ (data) => {
7193
+ if (!iterIO[key]) iterIO[key] = {};
7194
+ iterIO[key].outputs = formatData(data);
7195
+ },
7196
+ (err) => {
7197
+ if (!iterIO[key]) iterIO[key] = {};
7198
+ const msg = err instanceof Error ? err.message : String(err);
7199
+ iterIO[key].outputs = `(fetch error: ${msg.slice(0, 100)})`;
7200
+ }
7201
+ )
7202
+ );
7203
+ }
7204
+ }
7205
+ await Promise.all(fetchPromises);
7206
+ const failedCount = filtered.filter(
7207
+ (r) => r.properties.status === "Failed" || r.properties.status === "TimedOut"
7208
+ ).length;
7209
+ const succeededCount = filtered.filter(
7210
+ (r) => r.properties.status === "Succeeded"
7211
+ ).length;
7212
+ const lines = [
7213
+ `Repetitions for "${parsed.actionName}": ${totalCount} total (${succeededCount} succeeded, ${failedCount} failed)`
7214
+ ];
7215
+ if (totalCount > maxIter) {
7216
+ lines.push(` (showing first ${maxIter} of ${totalCount})`);
7217
+ }
7218
+ for (const rep of filtered) {
7219
+ const props = rep.properties;
7220
+ const startTime = new Date(props.startTime);
7221
+ const endTime = props.endTime ? new Date(props.endTime) : void 0;
7222
+ const durationMs = endTime ? endTime.getTime() - startTime.getTime() : void 0;
7223
+ const duration = durationMs ? `${(durationMs / 1e3).toFixed(1)}s` : "";
7224
+ const indexes = props.repetitionIndexes?.map((ri) => `${ri.scopeName}[${ri.itemIndex}]`).join(" > ");
7225
+ const status = props.status === "Succeeded" ? "\u2713" : props.status === "Failed" ? "\u2717" : props.status === "TimedOut" ? "\u23F1" : "\u25CB";
7226
+ let line = `
7227
+ ${status} ${indexes ?? rep.name} ${duration}`;
7228
+ if (props.error) {
7229
+ line += `
7230
+ Error: [${props.error.code}] ${props.error.message.slice(0, 500)}`;
7231
+ }
7232
+ const io = iterIO[rep.name];
7233
+ if (io?.inputs) {
7234
+ line += `
7235
+ Inputs: ${io.inputs}`;
7236
+ }
7237
+ if (io?.outputs) {
7238
+ line += `
7239
+ Outputs: ${io.outputs}`;
6768
7240
  }
6769
7241
  lines.push(line);
6770
7242
  }
@@ -12721,6 +13193,7 @@ var tools = [
12721
13193
  // Phase 4: Connections, Diagnostics, Sharing
12722
13194
  listConnectionsTool,
12723
13195
  getRunActionsTool,
13196
+ getRunActionRepetitionsTool,
12724
13197
  getFlowPermissionsTool,
12725
13198
  shareFlowTool,
12726
13199
  // Phase 5: Run Control, Environment Management
@@ -12856,6 +13329,9 @@ async function executeTool(context, toolName, input) {
12856
13329
  case "get_run_actions":
12857
13330
  result = await handleGetRunActions(context.flowApi, input);
12858
13331
  break;
13332
+ case "get_run_action_repetitions":
13333
+ result = await handleGetRunActionRepetitions(context.flowApi, input);
13334
+ break;
12859
13335
  case "get_flow_permissions":
12860
13336
  result = await handleGetFlowPermissions(context.flowApi, input);
12861
13337
  break;