powerplatform-mcp
Version:
PowerPlatform Model Context Protocol server
337 lines (336 loc) • 16.6 kB
JavaScript
import { readFileSync } from 'node:fs';
import { outputResult } from '../output.js';
export function registerPluginCommands(program, registry) {
program
.command('plugin-packages')
.description('List plugin packages in the environment')
.option('--include-managed', 'Include managed packages')
.option('--max <number>', 'Maximum records', '100')
.action(async (opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const result = await service.getPluginPackages(opts.includeManaged ?? false, parseInt(opts.max, 10));
const nameList = result.packages
.slice(0, 10)
.map((p) => `${p.name} v${p.version}`)
.join(', ');
outputResult({
fileName: 'plugin-packages',
data: result,
summary: [
`Found ${result.totalCount} plugin packages.`,
result.totalCount > 0 ? `Packages: ${nameList}${result.totalCount > 10 ? ', ...' : ''}` : '',
].filter(Boolean).join('\n'),
}, ctx.environmentName);
});
program
.command('register-plugin-package <filePath>')
.description('Register a new plugin package (.nupkg) in Dataverse')
.requiredOption('--name <name>', 'Display name for the package')
.requiredOption('--unique-name <uniqueName>', 'Unique name for the package')
.option('--pkg-version <version>', 'Package version', '1.0.0')
.option('--solution <name>', 'Solution unique name to add the component to')
.action(async (filePath, opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
console.log(`Reading plugin package from ${filePath}...`);
const content = readFileSync(filePath).toString('base64');
console.log(`Uploading plugin package (${(content.length * 0.75 / 1024).toFixed(0)} KB)...`);
const result = await service.registerPluginPackage({
name: opts.name,
uniqueName: opts.uniqueName,
version: opts.pkgVersion,
content,
solutionName: opts.solution,
});
outputResult({
fileName: `register-plugin-package-${opts.uniqueName}`,
data: result,
summary: [
`Registered plugin package:`,
` Name: ${opts.name}`,
` Unique Name: ${opts.uniqueName}`,
` Version: ${opts.pkgVersion}`,
` Package ID: ${result.pluginPackageId}`,
].join('\n'),
}, ctx.environmentName);
});
program
.command('update-plugin-package <filePath>')
.description('Update an existing plugin package with new content')
.requiredOption('--plugin-package-id <id>', 'ID of the existing plugin package')
.option('--pkg-version <version>', 'New version string')
.action(async (filePath, opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
console.log(`Reading plugin package from ${filePath}...`);
const content = readFileSync(filePath).toString('base64');
console.log(`Uploading updated plugin package (${(content.length * 0.75 / 1024).toFixed(0)} KB)...`);
await service.updatePluginPackage({
pluginPackageId: opts.pluginPackageId,
content,
version: opts.pkgVersion,
});
console.log(`Success: plugin package ${opts.pluginPackageId} updated`);
});
program
.command('entity-pipeline <entityName>')
.description('Get plugin pipeline for an entity, organized by message and stage')
.option('--message <message>', 'Filter by message (Create, Update, Delete)')
.option('--include-disabled', 'Include disabled steps')
.action(async (entityName, opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const result = await service.getEntityPluginPipeline(entityName, opts.message, opts.includeDisabled ?? false);
const suffix = opts.message ? `-${opts.message.toLowerCase()}` : '';
// Count steps by stage
const stages = { preValidation: 0, preOperation: 0, postOperation: 0 };
for (const step of result.steps) {
if (step.stage === 10)
stages.preValidation++;
else if (step.stage === 20)
stages.preOperation++;
else if (step.stage === 40)
stages.postOperation++;
}
const orderPreview = result.executionOrder
.slice(0, 5)
.map((name, i) => ` ${i + 1}. ${name}`);
const moreCount = result.executionOrder.length - 5;
outputResult({
fileName: `${entityName}-plugin-pipeline${suffix}`,
data: result,
summary: [
`Plugin pipeline for '${entityName}'${opts.message ? ` (${opts.message})` : ''}:`,
` Total steps: ${result.steps.length}`,
` Messages: ${result.messages.length}`,
` PreValidation: ${stages.preValidation}, PreOperation: ${stages.preOperation}, PostOperation: ${stages.postOperation}`,
result.executionOrder.length > 0 ? ` Execution order:` : '',
...orderPreview,
moreCount > 0 ? ` ... and ${moreCount} more` : '',
].filter(Boolean).join('\n'),
}, ctx.environmentName);
});
program
.command('plugin-assemblies')
.description('Get all plugin assemblies in the environment')
.option('--include-managed', 'Include managed assemblies')
.option('--max <number>', 'Maximum records', '100')
.action(async (opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const result = await service.getPluginAssemblies(opts.includeManaged ?? false, parseInt(opts.max, 10));
const nameList = result.assemblies
.slice(0, 10)
.map((a) => a.name)
.join(', ');
outputResult({
fileName: 'plugin-assemblies',
data: result,
summary: [
`Found ${result.totalCount} plugin assemblies.`,
`Assemblies: ${nameList}${result.totalCount > 10 ? ', ...' : ''}`,
].join('\n'),
}, ctx.environmentName);
});
program
.command('plugin-assembly <assemblyName>')
.description('Get a plugin assembly with all types, steps, and images')
.option('--include-disabled', 'Include disabled steps')
.action(async (assemblyName, opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const result = await service.getPluginAssemblyComplete(assemblyName, opts.includeDisabled ?? false);
const issues = result.validation.potentialIssues;
outputResult({
fileName: `plugin-assembly-${assemblyName.toLowerCase().replace(/\s+/g, '-')}`,
data: result,
summary: [
`Plugin assembly: '${assemblyName}'`,
` Plugin Types: ${result.pluginTypes.length}`,
` Steps: ${result.steps.length}`,
` Has Async Steps: ${result.validation.hasAsyncSteps}`,
` Has Sync Steps: ${result.validation.hasSyncSteps}`,
issues.length > 0 ? ` Issues: ${issues.join('; ')}` : ' Issues: None',
].join('\n'),
}, ctx.environmentName);
});
program
.command('plugin-trace-logs')
.description('Get plugin trace logs with filtering')
.option('--entity <name>', 'Filter by entity name')
.option('--message <name>', 'Filter by message (Create, Update, Delete)')
.option('--correlation-id <id>', 'Filter by correlation ID')
.option('--step-id <id>', 'Filter by plugin step ID')
.option('--exceptions-only', 'Only show logs with exceptions')
.option('--hours <number>', 'Hours to look back', '24')
.option('--max <number>', 'Maximum records', '50')
.action(async (opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const result = await service.getPluginTraceLogs({
entityName: opts.entity,
messageName: opts.message,
correlationId: opts.correlationId,
pluginStepId: opts.stepId,
exceptionOnly: opts.exceptionsOnly ?? false,
hoursBack: parseInt(opts.hours, 10),
maxRecords: parseInt(opts.max, 10),
});
const exceptionCount = result.logs
.filter((log) => log.parsed?.hasException).length;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
outputResult({
fileName: `plugin-trace-logs-${timestamp}`,
data: result,
summary: [
`Plugin trace logs (last ${opts.hours}h):`,
` Total logs: ${result.totalCount}`,
` Exceptions: ${exceptionCount}`,
opts.entity ? ` Entity filter: ${opts.entity}` : '',
opts.message ? ` Message filter: ${opts.message}` : '',
].filter(Boolean).join('\n'),
}, ctx.environmentName);
});
program
.command('plugin-type <typeName>')
.description('Look up a plugin type by its fully qualified class name')
.action(async (typeName, _opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const pluginType = await service.getPluginType(typeName);
if (!pluginType) {
console.error(`Plugin type '${typeName}' not found.`);
process.exit(1);
}
outputResult({
fileName: `plugin-type-${typeName.replace(/\./g, '-')}`,
data: pluginType,
summary: [
`Plugin Type: ${pluginType.typename}`,
` ID: ${pluginType.plugintypeid}`,
` Assembly: ${pluginType.assemblyname ?? 'N/A'}`,
` Friendly Name: ${pluginType.friendlyname ?? 'N/A'}`,
].join('\n'),
}, ctx.environmentName);
});
program
.command('sdk-message <messageName>')
.description('Look up an SDK message by name (e.g. Create, Update, br_SyncProperties)')
.action(async (messageName, _opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const message = await service.getSdkMessage(messageName);
if (!message) {
console.error(`SDK message '${messageName}' not found.`);
process.exit(1);
}
outputResult({
fileName: `sdk-message-${messageName}`,
data: message,
summary: [
`SDK Message: ${message.name}`,
` ID: ${message.sdkmessageid}`,
` Category: ${message.categoryname ?? 'N/A'}`,
` Active: ${message.isactive}`,
].join('\n'),
}, ctx.environmentName);
});
program
.command('create-plugin-step <name> <pluginTypeId> <sdkMessageId>')
.description('Register a new plugin step (SDK message processing step)')
.option('--stage <n>', 'Execution stage: 10=PreValidation, 20=PreOperation, 40=PostOperation', '40')
.option('--mode <n>', 'Execution mode: 0=Synchronous, 1=Asynchronous', '0')
.option('--rank <n>', 'Execution order', '1')
.option('--supported-deployment <n>', '0=ServerOnly, 1=OfflineOnly, 2=Both', '0')
.option('--description <desc>', 'Step description')
.option('--configuration <config>', 'Unsecure configuration string')
.option('--message-filter-id <id>', 'SDK message filter ID (entity filter)')
.option('--solution <name>', 'Solution unique name to add the component to')
.action(async (name, pluginTypeId, sdkMessageId, opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const stage = parseInt(opts.stage, 10);
const mode = parseInt(opts.mode, 10);
const result = await service.createPluginStep({
name, pluginTypeId, sdkMessageId,
stage, mode,
rank: parseInt(opts.rank, 10),
supportedDeployment: parseInt(opts.supportedDeployment, 10),
description: opts.description,
configuration: opts.configuration,
sdkMessageFilterId: opts.messageFilterId,
solutionName: opts.solution,
});
const stageName = stage === 10 ? 'PreValidation' : stage === 20 ? 'PreOperation' : 'PostOperation';
const modeName = mode === 0 ? 'Synchronous' : 'Asynchronous';
outputResult({
fileName: `create-plugin-step-${name.replace(/\s+/g, '-').toLowerCase()}`,
data: result,
summary: [
`Created plugin step:`,
` Name: ${name}`,
` Stage: ${stageName} (${stage})`,
` Mode: ${modeName} (${mode})`,
` Step ID: ${result.stepId}`,
].join('\n'),
}, ctx.environmentName);
});
program
.command('create-plugin-step-image <stepId>')
.description('Register a PreImage or PostImage on an SDK message processing step')
.option('--name <name>', 'Image name (plugin reads by this key)', 'PreImage')
.option('--entity-alias <alias>', 'Entity alias (defaults to --name)')
.option('--image-type <n>', '0=PreImage, 1=PostImage, 2=Both', '0')
.option('--message-property-name <name>', 'Target property for CRUD messages', 'Target')
.option('--attributes <csv>', 'Comma-separated schema names of columns to include')
.action(async (stepId, opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const imageType = parseInt(opts.imageType, 10);
const result = await service.createPluginStepImage({
stepId,
name: opts.name,
entityAlias: opts.entityAlias,
imageType,
messagePropertyName: opts.messagePropertyName,
attributes: opts.attributes,
});
const typeName = imageType === 0 ? 'PreImage' : imageType === 1 ? 'PostImage' : 'Both';
outputResult({
fileName: `create-plugin-step-image-${opts.name}`,
data: result,
summary: [
`Created plugin step image:`,
` Name: ${opts.name}`,
` Type: ${typeName} (${imageType})`,
` Step: ${stepId}`,
opts.attributes ? ` Attributes: ${opts.attributes}` : ` Attributes: (all)`,
` Image ID: ${result.imageId}`,
].join('\n'),
}, ctx.environmentName);
});
program
.command('all-plugin-steps')
.description('List all plugin SDK message processing steps across all assemblies')
.option('--include-disabled', 'Include disabled steps (included by default)')
.option('--max <number>', 'Maximum records', '10000')
.action(async (opts, command) => {
const ctx = registry.getContext(command.optsWithGlobals().env);
const service = ctx.getPluginService();
const result = await service.getAllPluginSteps({
includeDisabled: opts.includeDisabled !== false,
maxRecords: parseInt(opts.max, 10),
});
const enabledCount = result.steps.filter((s) => s.enabled).length;
const disabledCount = result.steps.filter((s) => !s.enabled).length;
outputResult({
fileName: 'all-plugin-steps',
data: result,
summary: [
`Found ${result.totalCount} plugin steps:`,
` Enabled: ${enabledCount}, Disabled: ${disabledCount}`,
].join('\n'),
}, ctx.environmentName);
});
}