powerplatform-mcp
Version:
PowerPlatform Model Context Protocol server
273 lines (272 loc) • 13.4 kB
JavaScript
import { readFileSync } from 'fs';
import { outputResult } from '../output.js';
export function registerFlowCommands(program, registry) {
program
.command('flows')
.description('List Power Automate cloud flows')
.option('--active', 'Only show active flows')
.option('--name <contains>', 'Filter by name (contains)')
.option('--include-managed', 'Include managed flows')
.option('--max <number>', 'Maximum records', '25')
.action(async (opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.getFlows({
activeOnly: opts.active ?? false,
nameContains: opts.name,
maxRecords: parseInt(opts.max, 10),
});
const nameList = result.flows
.slice(0, 10)
.map((f) => `${f.name} (${f.state})`)
.join('\n ');
outputResult({
fileName: 'flows',
data: result,
summary: [
`Found ${result.totalCount} flows${result.hasMore ? ' (more available)' : ''}:`,
result.excluded.total > 0 ? ` Excluded: ${result.excluded.total} (system: ${result.excluded.system}, copilot: ${result.excluded.copilotSales})` : '',
` Flows:\n ${nameList}${result.totalCount > 10 ? '\n ...' : ''}`,
].filter(Boolean).join('\n'),
}, ctx.environmentName);
});
program
.command('flow-definition <flowId>')
.description('Get a flow with its complete definition')
.option('--summary', 'Return summary instead of full definition')
.action(async (flowId, opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.getFlowDefinition(flowId, opts.summary ?? false);
const flowSummary = result.summary;
const summaryLines = [
`Flow: ${result.name}`,
` State: ${result.state}`,
` Type: ${result.type}`,
result.description ? ` Description: ${result.description}` : '',
result.primaryEntity ? ` Primary Entity: ${result.primaryEntity}` : '',
];
if (flowSummary) {
summaryLines.push(` Trigger: ${flowSummary.triggerInfo}`, ` Actions: ${flowSummary.actionCount}`, flowSummary.hasConditions ? ' Has conditions: yes' : '', flowSummary.hasLoops ? ' Has loops: yes' : '', flowSummary.hasErrorHandling ? ' Has error handling: yes' : '', Array.isArray(flowSummary.connectors) && flowSummary.connectors.length > 0
? ` Connectors: ${flowSummary.connectors.join(', ')}`
: '', Array.isArray(flowSummary.tablesModified) && flowSummary.tablesModified.length > 0
? ` Tables modified: ${flowSummary.tablesModified.join(', ')}`
: '', Array.isArray(flowSummary.customApisCalled) && flowSummary.customApisCalled.length > 0
? ` Custom APIs: ${flowSummary.customApisCalled.join(', ')}`
: '');
}
outputResult({
fileName: `flow-${flowId}-definition`,
data: result,
summary: summaryLines.filter(Boolean).join('\n'),
}, ctx.environmentName);
});
program
.command('flow-runs <flowId>')
.description('Get run history for a Power Automate flow')
.option('--status <status>', 'Filter by status (Succeeded, Failed, Running, Waiting, Cancelled)')
.option('--after <date>', 'Only runs started after this date (ISO 8601)')
.option('--before <date>', 'Only runs started before this date (ISO 8601)')
.option('--max <number>', 'Maximum records', '50')
.action(async (flowId, opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.getFlowRuns(flowId, {
status: opts.status,
startedAfter: opts.after,
startedBefore: opts.before,
maxRecords: parseInt(opts.max, 10),
});
// Count by status
const statusCounts = {};
for (const run of result.runs) {
statusCounts[run.status] = (statusCounts[run.status] || 0) + 1;
}
const statusSummary = Object.entries(statusCounts)
.map(([s, c]) => `${s}: ${c}`)
.join(', ');
outputResult({
fileName: `flow-${flowId}-runs`,
data: result,
summary: [
`Flow runs for ${flowId}${result.hasMore ? ' (more available)' : ''}:`,
` Total: ${result.totalCount}`,
` By status: ${statusSummary}`,
].join('\n'),
}, ctx.environmentName);
});
program
.command('flow-run-details <flowId> <runId>')
.description('Get detailed flow run info including action-level outputs')
.action(async (flowId, runId, _opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.getFlowRunDetails(flowId, runId);
const actionsSummary = result.actionsSummary;
const failedErrors = result.failedActionErrors;
const summaryLines = [
`Flow run ${runId}:`,
` Status: ${result.status}`,
` Actions: ${actionsSummary.total} (succeeded: ${actionsSummary.succeeded}, failed: ${actionsSummary.failed}, skipped: ${actionsSummary.skipped})`,
];
if (failedErrors.length > 0) {
summaryLines.push(' Failed actions:');
for (const err of failedErrors.slice(0, 5)) {
summaryLines.push(` - ${err.action}: ${err.message}`);
}
if (failedErrors.length > 5) {
summaryLines.push(` ... and ${failedErrors.length - 5} more`);
}
}
outputResult({
fileName: `flow-${flowId}-run-${runId}`,
data: result,
summary: summaryLines.join('\n'),
}, ctx.environmentName);
});
program
.command('search-workflows')
.description('Search workflows (classic and Power Automate)')
.option('--name <name>', 'Search by name (contains)')
.option('--entity <entity>', 'Filter by primary entity')
.option('--category <number>', 'Filter by category (0=Classic, 5=Flow)')
.option('--active', 'Only show active workflows')
.option('--max <number>', 'Maximum results', '50')
.action(async (opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.searchWorkflows({
name: opts.name,
primaryEntity: opts.entity,
category: opts.category ? parseInt(opts.category, 10) : undefined,
statecode: opts.active ? 1 : undefined,
maxResults: parseInt(opts.max, 10),
});
const nameList = result.workflows
.slice(0, 10)
.map((w) => `${w.name} (${w.category})`)
.join('\n ');
outputResult({
fileName: 'search-workflows',
data: result,
summary: [
`Found ${result.totalCount} workflows${result.hasMore ? ' (more available)' : ''}:`,
result.totalCount > 0 ? ` Workflows:\n ${nameList}${result.totalCount > 10 ? '\n ...' : ''}` : '',
].filter(Boolean).join('\n'),
}, ctx.environmentName);
});
program
.command('flow-health')
.description('Scan all cloud flows for health metrics (success rate, failures)')
.option('--days <number>', 'Days of run history to analyze', '7')
.option('--max-runs <number>', 'Max runs to check per flow', '100')
.option('--max-flows <number>', 'Max flows to scan', '500')
.option('--active', 'Only scan activated flows (default: true)')
.action(async (opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.scanFlowHealth({
daysBack: parseInt(opts.days, 10),
maxRunsPerFlow: parseInt(opts.maxRuns, 10),
maxFlows: parseInt(opts.maxFlows, 10),
activeOnly: opts.active !== false,
});
const topList = result.topFailingFlows
.slice(0, 5)
.map((f) => ` ${f.flowName}: ${f.failedRuns} failures (${f.successRate}% success)`)
.join('\n');
outputResult({
fileName: 'flow-health-scan',
data: result,
summary: [
`Flow health scan (last ${opts.days} days):`,
` Total scanned: ${result.summary.totalFlowsScanned}`,
` Healthy: ${result.summary.flowsHealthy}, Failing: ${result.summary.flowsWithFailures}, No runs: ${result.summary.flowsNoRuns}`,
` Overall success rate: ${result.summary.overallSuccessRate}%`,
result.topFailingFlows.length > 0 ? ` Top failing flows:\n${topList}` : '',
].filter(Boolean).join('\n'),
}, ctx.environmentName);
});
program
.command('flow-inventory')
.description('Get complete inventory of all cloud flows (deployment metadata, no run history)')
.option('--max <number>', 'Maximum flows to return', '500')
.action(async (opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.getFlowInventory({
maxRecords: parseInt(opts.max, 10),
});
const stateCounts = {};
for (const flow of result.flows) {
stateCounts[flow.state] = (stateCounts[flow.state] || 0) + 1;
}
const stateSummary = Object.entries(stateCounts)
.map(([s, c]) => `${s}: ${c}`)
.join(', ');
outputResult({
fileName: 'flow-inventory',
data: result,
summary: [
`Flow inventory: ${result.totalCount} flows`,
` By state: ${stateSummary}`,
result.excluded > 0 ? ` Excluded: ${result.excluded}` : '',
].filter(Boolean).join('\n'),
}, ctx.environmentName);
});
program
.command('create-cloud-flow <clientDataFile>')
.description('Create a modern Cloud Flow (workflow category=5) from a clientdata JSON file. Flow is created in Draft; activate separately.')
.requiredOption('--name <name>', 'Flow display name')
.option('--primary-entity <entity>', "Primary entity logical name (default: 'none')")
.option('--solution <name>', 'Solution unique name (uses MSCRM.SolutionUniqueName so no separate add-solution-component is needed)')
.action(async (clientDataFile, opts, command) => {
const clientData = readFileSync(clientDataFile, 'utf-8');
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.createCloudFlow({
name: opts.name,
clientData,
primaryEntity: opts.primaryEntity,
solutionName: opts.solution,
});
outputResult({
fileName: `create-cloud-flow-${opts.name.replace(/\s+/g, '-').toLowerCase()}`,
data: result,
summary: [
`Created Cloud Flow (Draft):`,
` Name: ${opts.name}`,
opts.solution ? ` Solution: ${opts.solution}` : '',
` Flow ID: ${result.flowId}`,
'',
'Next step: activate with `activate-flow <flowId>` (or from the portal if the flow has user-owned connection references).',
].filter(Boolean).join('\n'),
}, ctx.environmentName);
});
program
.command('activate-flow <flowId>')
.description('Activate a Cloud Flow (set statecode=1, statuscode=2)')
.action(async (flowId, _opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.setFlowState(flowId, true);
outputResult({
fileName: `flow-${flowId}-activate`,
data: result,
summary: `Flow ${flowId}: ${result.previousState} -> ${result.newState}`,
}, ctx.environmentName);
});
program
.command('deactivate-flow <flowId>')
.description('Deactivate a Cloud Flow (set statecode=0, statuscode=1)')
.action(async (flowId, _opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getFlowService();
const result = await service.setFlowState(flowId, false);
outputResult({
fileName: `flow-${flowId}-deactivate`,
data: result,
summary: `Flow ${flowId}: ${result.previousState} -> ${result.newState}`,
}, ctx.environmentName);
});
}