powerautomate-mcp 0.7.9 → 0.8.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
  */
@@ -4212,11 +4246,15 @@ var listFlowsInputSchema = z.object({
4212
4246
  environment: z.string().optional().describe("Environment name or GUID. Uses default if omitted."),
4213
4247
  search: z.string().optional().describe("Search flows by display name (case-insensitive substring match). Applied client-side after fetching."),
4214
4248
  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)")
4249
+ top: z.number().min(1).max(250).optional().describe("Maximum number of results to return (default 50, max 250)"),
4250
+ scope: z.enum(["owned", "shared", "all"]).optional().default("all").describe(
4251
+ "Filter by ownership: 'owned' = only flows you created, 'shared' = only flows shared with you, 'all' = both (default)."
4252
+ ),
4253
+ includeOwner: z.boolean().optional().default(true).describe("Include owner/creator name in the output (default true).")
4216
4254
  });
4217
4255
  var listFlowsTool = {
4218
4256
  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.",
4257
+ 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
4258
  inputSchema: {
4221
4259
  type: "object",
4222
4260
  properties: {
@@ -4237,6 +4275,17 @@ var listFlowsTool = {
4237
4275
  description: "Maximum number of results to return (default 50, max 250)",
4238
4276
  minimum: 1,
4239
4277
  maximum: 250
4278
+ },
4279
+ scope: {
4280
+ type: "string",
4281
+ enum: ["owned", "shared", "all"],
4282
+ description: "Filter by ownership: 'owned' = only flows you created, 'shared' = only flows shared with you, 'all' = both (default).",
4283
+ default: "all"
4284
+ },
4285
+ includeOwner: {
4286
+ type: "boolean",
4287
+ description: "Include owner/creator name in the output (default true).",
4288
+ default: true
4240
4289
  }
4241
4290
  }
4242
4291
  }
@@ -4262,14 +4311,39 @@ async function handleListFlows(api, input) {
4262
4311
  const needle = parsed.search.toLowerCase();
4263
4312
  flows = flows.filter((f) => f.properties.displayName.toLowerCase().includes(needle));
4264
4313
  }
4314
+ let currentUserOid;
4315
+ if (parsed.scope !== "all" || parsed.includeOwner) {
4316
+ currentUserOid = await api.getCurrentUserOid();
4317
+ if (parsed.scope !== "all" && !currentUserOid) {
4318
+ logger.warn("list_flows: could not derive current user OID; scope filter will not apply");
4319
+ }
4320
+ }
4321
+ if (parsed.scope !== "all" && currentUserOid) {
4322
+ flows = flows.filter((f) => {
4323
+ const creatorId = f.properties.creator?.id;
4324
+ if (!creatorId) return false;
4325
+ const isOwned = creatorId === currentUserOid;
4326
+ return parsed.scope === "owned" ? isOwned : !isOwned;
4327
+ });
4328
+ }
4265
4329
  if (flows.length === 0) {
4266
- return "No flows found";
4330
+ 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
4331
  }
4268
- const lines = [`Flows (${flows.length}):`];
4332
+ const scopeLabel = parsed.scope === "owned" ? "Owned" : parsed.scope === "shared" ? "Shared with you" : "Flows";
4333
+ const lines = [`${scopeLabel} (${flows.length}):`];
4269
4334
  for (const flow of flows) {
4270
4335
  const state = flow.properties.state === "Started" ? "\u25B6" : "\u23F9";
4271
4336
  const modified = new Date(flow.properties.lastModifiedTime).toLocaleDateString();
4272
- lines.push(` ${state} ${flow.properties.displayName} (${flow.name}) - ${modified}`);
4337
+ let line = ` ${state} ${flow.properties.displayName} (${flow.name}) - ${modified}`;
4338
+ if (parsed.includeOwner) {
4339
+ const creator = flow.properties.creator;
4340
+ const ownerName = creator?.displayName ?? creator?.email;
4341
+ const isOwnedTag = currentUserOid && creator?.id === currentUserOid ? " [owned]" : creator ? " [shared]" : "";
4342
+ if (ownerName) {
4343
+ line += ` \u2014 ${ownerName}${isOwnedTag}`;
4344
+ }
4345
+ }
4346
+ lines.push(line);
4273
4347
  }
4274
4348
  if (result.nextLink) {
4275
4349
  lines.push(`
@@ -4280,11 +4354,14 @@ async function handleListFlows(api, input) {
4280
4354
  var getFlowInputSchema = z.object({
4281
4355
  flowId: z.string().describe("The flow ID (GUID) to retrieve"),
4282
4356
  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)")
4357
+ includeDefinition: z.boolean().optional().default(true).describe("Include the full flow definition JSON (default true)"),
4358
+ format: z.enum(["summary", "json", "both"]).optional().default("summary").describe(
4359
+ "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."
4360
+ )
4284
4361
  });
4285
4362
  var getFlowTool = {
4286
4363
  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.",
4364
+ 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
4365
  inputSchema: {
4289
4366
  type: "object",
4290
4367
  properties: {
@@ -4300,6 +4377,12 @@ var getFlowTool = {
4300
4377
  type: "boolean",
4301
4378
  description: "Include the full flow definition JSON (default true)",
4302
4379
  default: true
4380
+ },
4381
+ format: {
4382
+ type: "string",
4383
+ enum: ["summary", "json", "both"],
4384
+ description: "Output format: 'summary' = top-level overview (default), 'json' = full raw definition with all nested actions, 'both' = summary plus full JSON.",
4385
+ default: "summary"
4303
4386
  }
4304
4387
  },
4305
4388
  required: ["flowId"]
@@ -4307,7 +4390,7 @@ var getFlowTool = {
4307
4390
  };
4308
4391
  async function handleGetFlow(api, input) {
4309
4392
  const parsed = getFlowInputSchema.parse(input);
4310
- logger.info({ flowId: parsed.flowId }, "Executing get_flow");
4393
+ logger.info({ flowId: parsed.flowId, format: parsed.format }, "Executing get_flow");
4311
4394
  const flow = await api.getFlow(parsed.flowId, parsed.environment);
4312
4395
  const triggers = flow.properties.definition.triggers;
4313
4396
  const actions = flow.properties.definition.actions;
@@ -4320,6 +4403,22 @@ async function handleGetFlow(api, input) {
4320
4403
  tier: ref.tier
4321
4404
  })
4322
4405
  );
4406
+ const description = flow.properties.description;
4407
+ const creator = flow.properties.creator;
4408
+ if (parsed.format === "json") {
4409
+ const payload = {
4410
+ id: flow.name,
4411
+ displayName: flow.properties.displayName,
4412
+ description,
4413
+ state: flow.properties.state,
4414
+ createdTime: flow.properties.createdTime,
4415
+ lastModifiedTime: flow.properties.lastModifiedTime,
4416
+ creator,
4417
+ connectionReferences: flow.properties.connectionReferences,
4418
+ definition: flow.properties.definition
4419
+ };
4420
+ return JSON.stringify(payload, null, 2);
4421
+ }
4323
4422
  const lines = [
4324
4423
  `## ${flow.properties.displayName}`,
4325
4424
  ``,
@@ -4329,6 +4428,12 @@ async function handleGetFlow(api, input) {
4329
4428
  `**Actions:** ${Object.keys(actions).length}`,
4330
4429
  `**Modified:** ${new Date(flow.properties.lastModifiedTime).toLocaleDateString()}`
4331
4430
  ];
4431
+ if (description) {
4432
+ lines.push(`**Description:** ${description}`);
4433
+ }
4434
+ if (creator?.displayName || creator?.email) {
4435
+ lines.push(`**Owner:** ${creator.displayName ?? creator.email}${creator.id ? ` (${creator.id})` : ""}`);
4436
+ }
4332
4437
  if (connectionRefs.length > 0) {
4333
4438
  lines.push(``, `### Connections`);
4334
4439
  for (const conn of connectionRefs) {
@@ -4337,7 +4442,7 @@ async function handleGetFlow(api, input) {
4337
4442
  }
4338
4443
  }
4339
4444
  if (parsed.includeDefinition !== false) {
4340
- lines.push(``, `### Actions`);
4445
+ lines.push(``, `### Actions (top-level)`);
4341
4446
  for (const [name, action] of Object.entries(actions)) {
4342
4447
  const act = action;
4343
4448
  const deps = act.runAfter ? Object.keys(act.runAfter).join(", ") : "trigger";
@@ -4346,6 +4451,11 @@ async function handleGetFlow(api, input) {
4346
4451
  lines.push(` inputs: ${JSON.stringify(act.inputs, null, 2).split("\n").join("\n ")}`);
4347
4452
  }
4348
4453
  }
4454
+ lines.push(``);
4455
+ 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._`);
4456
+ }
4457
+ if (parsed.format === "both") {
4458
+ lines.push(``, `### Full Definition (JSON)`, "```json", JSON.stringify(flow.properties.definition, null, 2), "```");
4349
4459
  }
4350
4460
  return lines.join("\n");
4351
4461
  }
@@ -5802,6 +5912,7 @@ var connectionRefSchema = z.object({
5802
5912
  });
5803
5913
  var createFlowInputSchema = z.object({
5804
5914
  displayName: z.string().describe("The display name for the new flow"),
5915
+ description: z.string().optional().describe("Optional description for the flow"),
5805
5916
  trigger: triggerSchema.describe("The trigger definition"),
5806
5917
  triggerName: z.string().optional().default("manual").describe("Name for the trigger (default: 'manual')"),
5807
5918
  actions: z.record(actionSchema).describe("Actions in the flow, keyed by action name"),
@@ -5819,6 +5930,10 @@ var createFlowTool = {
5819
5930
  type: "string",
5820
5931
  description: "The display name for the new flow"
5821
5932
  },
5933
+ description: {
5934
+ type: "string",
5935
+ description: "Optional description shown in the Power Automate UI"
5936
+ },
5822
5937
  trigger: {
5823
5938
  type: "object",
5824
5939
  description: "The trigger definition with type, kind, inputs, and optional recurrence",
@@ -5946,7 +6061,8 @@ async function handleCreateFlow(api, input) {
5946
6061
  properties: {
5947
6062
  displayName: parsed.displayName,
5948
6063
  definition,
5949
- connectionReferences: Object.keys(connectionReferences).length > 0 ? connectionReferences : void 0
6064
+ connectionReferences: Object.keys(connectionReferences).length > 0 ? connectionReferences : void 0,
6065
+ ...parsed.description !== void 0 ? { description: parsed.description } : {}
5950
6066
  }
5951
6067
  };
5952
6068
  try {
@@ -6008,16 +6124,25 @@ var connectionRefSchema2 = z.object({
6008
6124
  var updateFlowInputSchema = z.object({
6009
6125
  flowId: z.string().describe("The flow ID (GUID) to update"),
6010
6126
  displayName: z.string().optional().describe("New display name for the flow"),
6127
+ description: z.string().optional().describe("New description (set to empty string to clear)"),
6011
6128
  trigger: triggerSchema2.optional().describe("New trigger definition (replaces existing)"),
6012
6129
  triggerName: z.string().optional().describe("Name for the trigger"),
6013
- actions: z.record(actionSchema2).optional().describe("New actions (replaces all existing actions)"),
6130
+ actions: z.record(actionSchema2).optional().describe(
6131
+ "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)."
6132
+ ),
6133
+ mergeActions: z.boolean().optional().default(false).describe(
6134
+ "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."
6135
+ ),
6136
+ patchActions: z.record(actionSchema2.nullable()).optional().describe(
6137
+ "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."
6138
+ ),
6014
6139
  connectionReferences: z.record(connectionRefSchema2).optional().describe("New connection references (replaces existing)"),
6015
6140
  environment: z.string().optional().describe("Environment name or GUID"),
6016
6141
  validateBeforeUpdate: z.boolean().optional().default(true).describe("Validate before updating")
6017
6142
  });
6018
6143
  var updateFlowTool = {
6019
6144
  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.",
6145
+ 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
6146
  inputSchema: {
6022
6147
  type: "object",
6023
6148
  properties: {
@@ -6029,6 +6154,10 @@ var updateFlowTool = {
6029
6154
  type: "string",
6030
6155
  description: "New display name for the flow"
6031
6156
  },
6157
+ description: {
6158
+ type: "string",
6159
+ description: "New description for the flow (empty string clears)"
6160
+ },
6032
6161
  trigger: {
6033
6162
  type: "object",
6034
6163
  description: "New trigger definition (replaces existing)"
@@ -6039,7 +6168,16 @@ var updateFlowTool = {
6039
6168
  },
6040
6169
  actions: {
6041
6170
  type: "object",
6042
- description: "New actions (replaces all existing actions)"
6171
+ description: "Actions. By default replaces all existing top-level actions. With mergeActions=true, deep-merges instead."
6172
+ },
6173
+ mergeActions: {
6174
+ type: "boolean",
6175
+ description: "When true, deep-merge 'actions' onto current flow instead of replacing. Recurses into nested actions/cases/else/foreach.",
6176
+ default: false
6177
+ },
6178
+ patchActions: {
6179
+ type: "object",
6180
+ description: "Path-based patch map. Keys: slash-separated paths like 'If/cases/Default/actions/Compose'. Value=null deletes the node. Smallest payload."
6043
6181
  },
6044
6182
  connectionReferences: {
6045
6183
  type: "object",
@@ -6057,17 +6195,80 @@ var updateFlowTool = {
6057
6195
  required: ["flowId"]
6058
6196
  }
6059
6197
  };
6198
+ function deepMerge(base, patch) {
6199
+ const out = { ...base };
6200
+ for (const [key, value] of Object.entries(patch)) {
6201
+ if (value === void 0) continue;
6202
+ const baseVal = out[key];
6203
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
6204
+ out[key] = deepMerge(
6205
+ baseVal,
6206
+ value
6207
+ );
6208
+ } else {
6209
+ out[key] = value;
6210
+ }
6211
+ }
6212
+ return out;
6213
+ }
6214
+ function applyPathPatch(actions, path, value) {
6215
+ const segments = path.split("/").filter(Boolean);
6216
+ if (segments.length === 0) return actions;
6217
+ const result = JSON.parse(JSON.stringify(actions));
6218
+ let cursor = result;
6219
+ for (let i = 0; i < segments.length - 1; i++) {
6220
+ const seg = segments[i];
6221
+ const next = cursor[seg];
6222
+ if (next === null || typeof next !== "object" || Array.isArray(next)) {
6223
+ cursor[seg] = {};
6224
+ }
6225
+ cursor = cursor[seg];
6226
+ }
6227
+ const lastSeg = segments[segments.length - 1];
6228
+ if (value === null) {
6229
+ delete cursor[lastSeg];
6230
+ } else {
6231
+ cursor[lastSeg] = value;
6232
+ }
6233
+ return result;
6234
+ }
6060
6235
  async function handleUpdateFlow(api, input) {
6061
6236
  const parsed = updateFlowInputSchema.parse(input);
6062
- logger.info({ flowId: parsed.flowId }, "Executing update_flow");
6237
+ logger.info(
6238
+ {
6239
+ flowId: parsed.flowId,
6240
+ mergeActions: parsed.mergeActions,
6241
+ hasPatch: !!parsed.patchActions
6242
+ },
6243
+ "Executing update_flow"
6244
+ );
6063
6245
  const currentFlow = await api.getFlow(parsed.flowId, parsed.environment);
6064
6246
  const currentDef = currentFlow.properties.definition;
6065
6247
  const currentConnRefs = currentFlow.properties.connectionReferences;
6066
6248
  const newDisplayName = parsed.displayName ?? currentFlow.properties.displayName;
6249
+ const newDescription = parsed.description !== void 0 ? parsed.description : currentFlow.properties.description;
6067
6250
  let newDefinition;
6068
- if (parsed.trigger || parsed.actions) {
6251
+ let resolvedActions;
6252
+ if (parsed.actions || parsed.patchActions) {
6253
+ if (parsed.mergeActions && parsed.actions) {
6254
+ resolvedActions = deepMerge(
6255
+ currentDef.actions,
6256
+ parsed.actions
6257
+ );
6258
+ } else if (parsed.actions) {
6259
+ resolvedActions = parsed.actions;
6260
+ } else {
6261
+ resolvedActions = JSON.parse(JSON.stringify(currentDef.actions));
6262
+ }
6263
+ if (parsed.patchActions) {
6264
+ for (const [path, value] of Object.entries(parsed.patchActions)) {
6265
+ resolvedActions = applyPathPatch(resolvedActions, path, value);
6266
+ }
6267
+ }
6268
+ }
6269
+ if (parsed.trigger || resolvedActions) {
6069
6270
  const triggerName = parsed.triggerName ?? Object.keys(currentDef.triggers)[0] ?? "manual";
6070
- const processedActions = parsed.actions ? preprocessFlowActions(parsed.actions) : currentDef.actions;
6271
+ const processedActions = resolvedActions ? preprocessFlowActions(resolvedActions) : currentDef.actions;
6071
6272
  newDefinition = {
6072
6273
  $schema: currentDef.$schema,
6073
6274
  contentVersion: currentDef.contentVersion,
@@ -6121,7 +6322,8 @@ async function handleUpdateFlow(api, input) {
6121
6322
  properties: {
6122
6323
  displayName: newDisplayName,
6123
6324
  definition: newDefinition,
6124
- connectionReferences: newConnRefs
6325
+ connectionReferences: newConnRefs,
6326
+ ...newDescription !== void 0 ? { description: newDescription } : {}
6125
6327
  }
6126
6328
  };
6127
6329
  try {