@mcp-consultant-tools/powerplatform-customization 25.0.0-beta.1 → 25.0.0-beta.2

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/build/index.js CHANGED
@@ -792,12 +792,39 @@ export function registerPowerplatformCustomizationTools(server, service) {
792
792
  };
793
793
  }
794
794
  });
795
- server.tool("update-attribute", "Update an existing attribute on an entity. Supports converting String attributes to AutoNumber by setting autoNumberFormat.", {
795
+ server.tool("update-attribute", "Update an existing attribute on an entity. Supports updating display properties, numeric constraints, date/time format, string format, and auditing settings.", {
796
796
  entityLogicalName: z.string().describe("Entity logical name"),
797
797
  attributeLogicalName: z.string().describe("Attribute logical name"),
798
798
  displayName: z.string().optional().describe("New display name"),
799
799
  description: z.string().optional().describe("New description"),
800
800
  requiredLevel: z.enum(["None", "Recommended", "ApplicationRequired"]).optional().describe("Required level"),
801
+ // String/Memo properties
802
+ maxLength: z.number().optional().describe("Maximum length for String/Memo attributes. " +
803
+ "String (single-line): 1-4000 characters. " +
804
+ "Memo (multi-line): 1-1048576 characters. " +
805
+ "Note: Reducing maxLength won't truncate existing data."),
806
+ formatName: z.enum(["Text", "TextArea", "Email", "Phone", "Url", "TickerSymbol"]).optional().describe("Format for String attributes. Text=single line, TextArea=multi-line, " +
807
+ "Email/Phone/Url/TickerSymbol=formatted input with validation."),
808
+ // Numeric properties (Integer, Decimal, Money)
809
+ minValue: z.number().optional().describe("Minimum value for Integer/Decimal/Money attributes. " +
810
+ "Integer: -2147483648 to 2147483647. " +
811
+ "Decimal: -100000000000 to 100000000000. " +
812
+ "Money: -922337203685477 to 922337203685477."),
813
+ maxValue: z.number().optional().describe("Maximum value for Integer/Decimal/Money attributes. " +
814
+ "Integer: -2147483648 to 2147483647. " +
815
+ "Decimal: -100000000000 to 100000000000. " +
816
+ "Money: -922337203685477 to 922337203685477."),
817
+ precision: z.number().optional().describe("Decimal precision for Decimal/Money attributes (0-10 decimal places)."),
818
+ precisionSource: z.enum(["Precision", "Pricing", "Currency"]).optional().describe("Precision source for Money attributes. " +
819
+ "Precision=use precision property, Pricing=org pricing setting, Currency=currency precision."),
820
+ // DateTime properties
821
+ format: z.enum(["DateAndTime", "DateOnly"]).optional().describe("Display format for DateTime attributes."),
822
+ dateTimeBehavior: z.enum(["UserLocal", "DateOnly", "TimeZoneIndependent"]).optional().describe("Time zone behavior for DateTime attributes. Only changeable if CanChangeDateTimeBehavior=true. " +
823
+ "UserLocal=adjusted to user timezone, DateOnly=date without time, TimeZoneIndependent=stored as-is."),
824
+ // Auditing and search properties
825
+ isAuditEnabled: z.boolean().optional().describe("Enable/disable auditing for this attribute. Tracks changes to field values."),
826
+ isValidForAdvancedFind: z.boolean().optional().describe("Show this attribute in Advanced Find queries."),
827
+ // AutoNumber conversion
801
828
  autoNumberFormat: z.string().optional().describe("Auto-number format string to convert String attribute to AutoNumber. " +
802
829
  "Use placeholders: {SEQNUM:n} for sequential number (min length n), " +
803
830
  "{RANDSTRING:n} for random alphanumeric (length 1-6 only), " +
@@ -823,6 +850,69 @@ export function registerPowerplatformCustomizationTools(server, service) {
823
850
  if (params.requiredLevel) {
824
851
  updates.RequiredLevel = { Value: params.requiredLevel, CanBeChanged: true };
825
852
  }
853
+ // Handle MaxLength update for String/Memo attributes
854
+ if (params.maxLength !== undefined) {
855
+ if (params.maxLength < 1) {
856
+ throw new Error("maxLength must be at least 1 character.");
857
+ }
858
+ if (params.maxLength > 1048576) {
859
+ throw new Error("maxLength cannot exceed 1,048,576 characters (Memo maximum).");
860
+ }
861
+ updates.MaxLength = params.maxLength;
862
+ }
863
+ // Handle FormatName update for String attributes
864
+ if (params.formatName) {
865
+ updates.FormatName = { Value: params.formatName };
866
+ }
867
+ // Handle MinValue update for Integer/Decimal/Money attributes
868
+ if (params.minValue !== undefined) {
869
+ updates.MinValue = params.minValue;
870
+ }
871
+ // Handle MaxValue update for Integer/Decimal/Money attributes
872
+ if (params.maxValue !== undefined) {
873
+ updates.MaxValue = params.maxValue;
874
+ }
875
+ // Handle Precision update for Decimal/Money attributes
876
+ if (params.precision !== undefined) {
877
+ if (params.precision < 0 || params.precision > 10) {
878
+ throw new Error("precision must be between 0 and 10 decimal places.");
879
+ }
880
+ updates.Precision = params.precision;
881
+ }
882
+ // Handle PrecisionSource update for Money attributes
883
+ if (params.precisionSource) {
884
+ const precisionSourceMap = {
885
+ "Precision": 0,
886
+ "Pricing": 1,
887
+ "Currency": 2
888
+ };
889
+ updates.PrecisionSource = precisionSourceMap[params.precisionSource];
890
+ }
891
+ // Handle Format update for DateTime attributes
892
+ if (params.format) {
893
+ updates.Format = params.format;
894
+ }
895
+ // Handle DateTimeBehavior update for DateTime attributes
896
+ if (params.dateTimeBehavior) {
897
+ updates.DateTimeBehavior = {
898
+ "@odata.type": "Microsoft.Dynamics.CRM.DateTimeBehavior",
899
+ Value: params.dateTimeBehavior
900
+ };
901
+ }
902
+ // Handle IsAuditEnabled update
903
+ if (params.isAuditEnabled !== undefined) {
904
+ updates.IsAuditEnabled = {
905
+ Value: params.isAuditEnabled,
906
+ CanBeChanged: true
907
+ };
908
+ }
909
+ // Handle IsValidForAdvancedFind update
910
+ if (params.isValidForAdvancedFind !== undefined) {
911
+ updates.IsValidForAdvancedFind = {
912
+ Value: params.isValidForAdvancedFind,
913
+ CanBeChanged: true
914
+ };
915
+ }
826
916
  // Handle AutoNumber format conversion
827
917
  if (params.autoNumberFormat) {
828
918
  // Validate RANDSTRING lengths (common error - API rejects length > 6)
@@ -845,10 +935,45 @@ export function registerPowerplatformCustomizationTools(server, service) {
845
935
  }
846
936
  await service.updateAttribute(params.entityLogicalName, params.attributeLogicalName, updates, params.solutionUniqueName);
847
937
  let successMessage = `✅ Successfully updated attribute '${params.attributeLogicalName}' on entity '${params.entityLogicalName}'`;
938
+ // Build list of changes made
939
+ const changes = [];
940
+ if (params.displayName)
941
+ changes.push(`Display name: "${params.displayName}"`);
942
+ if (params.description)
943
+ changes.push(`Description updated`);
944
+ if (params.requiredLevel)
945
+ changes.push(`Required level: ${params.requiredLevel}`);
946
+ if (params.maxLength !== undefined)
947
+ changes.push(`Max length: ${params.maxLength} characters`);
948
+ if (params.formatName)
949
+ changes.push(`Format: ${params.formatName}`);
950
+ if (params.minValue !== undefined)
951
+ changes.push(`Min value: ${params.minValue}`);
952
+ if (params.maxValue !== undefined)
953
+ changes.push(`Max value: ${params.maxValue}`);
954
+ if (params.precision !== undefined)
955
+ changes.push(`Precision: ${params.precision} decimal places`);
956
+ if (params.precisionSource)
957
+ changes.push(`Precision source: ${params.precisionSource}`);
958
+ if (params.format)
959
+ changes.push(`DateTime format: ${params.format}`);
960
+ if (params.dateTimeBehavior)
961
+ changes.push(`DateTime behavior: ${params.dateTimeBehavior}`);
962
+ if (params.isAuditEnabled !== undefined)
963
+ changes.push(`Auditing: ${params.isAuditEnabled ? 'enabled' : 'disabled'}`);
964
+ if (params.isValidForAdvancedFind !== undefined)
965
+ changes.push(`Advanced Find: ${params.isValidForAdvancedFind ? 'enabled' : 'disabled'}`);
966
+ if (params.autoNumberFormat)
967
+ changes.push(`AutoNumber format: ${params.autoNumberFormat}`);
968
+ if (changes.length > 0) {
969
+ successMessage += `\n\n📝 Changes made:\n${changes.map(c => ` • ${c}`).join('\n')}`;
970
+ }
848
971
  if (params.autoNumberFormat) {
849
- successMessage += `\n\n📋 Auto-number format set to: ${params.autoNumberFormat}`;
850
972
  successMessage += `\n\n⚠️ NOTE: Converting to AutoNumber is irreversible. The attribute will now auto-generate values based on the format.`;
851
973
  }
974
+ if (params.dateTimeBehavior) {
975
+ successMessage += `\n\n⚠️ NOTE: DateTimeBehavior changes may affect existing data interpretation. Review dependent workflows and business rules.`;
976
+ }
852
977
  successMessage += `\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`;
853
978
  return {
854
979
  content: [{ type: "text", text: successMessage }]
@@ -1983,43 +2108,51 @@ export function registerPowerplatformCustomizationTools(server, service) {
1983
2108
  solutionUniqueName: z.string().optional(),
1984
2109
  replaceExisting: z.boolean().optional().describe("Update existing assembly vs. create new"),
1985
2110
  }, async (params) => {
2111
+ const service = getPowerPlatformService();
2112
+ // Track created resources for potential rollback
2113
+ let createdAssemblyId = null;
2114
+ let isNewAssembly = false;
2115
+ const createdStepIds = [];
2116
+ const summary = {
2117
+ phases: {
2118
+ deploy: {},
2119
+ register: { stepsCreated: 0, imagesCreated: 0 },
2120
+ },
2121
+ };
2122
+ let dllBuffer;
1986
2123
  try {
1987
- const service = getPowerPlatformService();
1988
- const summary = {
1989
- phases: {
1990
- deploy: {},
1991
- register: { stepsCreated: 0, imagesCreated: 0 },
1992
- },
1993
- };
1994
2124
  // Read DLL file
1995
2125
  const fs = await import('fs/promises');
1996
2126
  const normalizedPath = params.assemblyPath.replace(/\\/g, '/');
1997
- const dllBuffer = await fs.readFile(normalizedPath);
2127
+ dllBuffer = await fs.readFile(normalizedPath);
1998
2128
  const dllBase64 = dllBuffer.toString('base64');
1999
2129
  const version = await service.extractAssemblyVersion(params.assemblyPath);
2000
- // Phase 1: Deploy assembly
2001
- if (params.replaceExisting) {
2002
- // Find existing assembly ID
2003
- const assemblyId = await service.queryPluginAssemblyByName(params.assemblyName);
2004
- if (assemblyId) {
2005
- await service.updatePluginAssembly(assemblyId, dllBase64, version, params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION);
2006
- summary.phases.deploy = {
2007
- action: 'updated',
2008
- assemblyId: assemblyId,
2009
- version,
2010
- };
2011
- }
2012
- else {
2013
- throw new Error(`Assembly '${params.assemblyName}' not found for update`);
2014
- }
2130
+ // Phase 1: Deploy assembly (UPSERT logic)
2131
+ const existingAssemblyId = await service.queryPluginAssemblyByName(params.assemblyName);
2132
+ if (existingAssemblyId) {
2133
+ // Assembly exists - update it (no rollback needed for updates)
2134
+ await service.updatePluginAssembly(existingAssemblyId, dllBase64, version, params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION);
2135
+ summary.phases.deploy = {
2136
+ action: 'updated',
2137
+ assemblyId: existingAssemblyId,
2138
+ version,
2139
+ pluginTypes: await service.getPluginTypesForAssembly(existingAssemblyId),
2140
+ };
2015
2141
  }
2016
2142
  else {
2143
+ // Assembly doesn't exist - create it
2144
+ if (params.replaceExisting) {
2145
+ // User expected update but assembly not found - warn but proceed with create
2146
+ console.error(`Warning: Assembly '${params.assemblyName}' not found for update. Creating new assembly instead.`);
2147
+ }
2017
2148
  const uploadResult = await service.createPluginAssembly({
2018
2149
  name: params.assemblyName,
2019
2150
  content: dllBase64,
2020
2151
  version,
2021
2152
  solutionUniqueName: params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION,
2022
2153
  });
2154
+ createdAssemblyId = uploadResult.pluginAssemblyId; // Track for rollback
2155
+ isNewAssembly = true;
2023
2156
  summary.phases.deploy = {
2024
2157
  action: 'created',
2025
2158
  assemblyId: uploadResult.pluginAssemblyId,
@@ -2027,7 +2160,7 @@ export function registerPowerplatformCustomizationTools(server, service) {
2027
2160
  pluginTypes: uploadResult.pluginTypes,
2028
2161
  };
2029
2162
  }
2030
- // Phase 2: Register steps
2163
+ // Phase 2: Register steps (with rollback tracking)
2031
2164
  if (params.stepConfigurations) {
2032
2165
  const stageMap = {
2033
2166
  PreValidation: 10,
@@ -2050,6 +2183,7 @@ export function registerPowerplatformCustomizationTools(server, service) {
2050
2183
  filteringAttributes: stepConfig.filteringAttributes?.join(','),
2051
2184
  solutionUniqueName: params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION,
2052
2185
  });
2186
+ createdStepIds.push(stepResult.stepId); // Track for rollback
2053
2187
  summary.phases.register.stepsCreated++;
2054
2188
  // Register pre-image
2055
2189
  if (stepConfig.preImage) {
@@ -2081,28 +2215,154 @@ export function registerPowerplatformCustomizationTools(server, service) {
2081
2215
  return {
2082
2216
  content: [{
2083
2217
  type: "text",
2084
- text: `✅ Plugin deployment completed successfully!\n\n` +
2085
- `📦 Assembly: ${summary.phases.deploy.action === 'created' ? 'Created' : 'Updated'}\n` +
2086
- `🆔 Assembly ID: ${summary.phases.deploy.assemblyId}\n` +
2087
- `🔢 Version: ${summary.phases.deploy.version}\n` +
2088
- `💾 Size: ${(dllBuffer.length / 1024).toFixed(2)} KB\n` +
2089
- (summary.phases.deploy.pluginTypes ? `🔌 Plugin Types: ${summary.phases.deploy.pluginTypes.length}\n` : '') +
2090
- `📝 Steps Created: ${summary.phases.register.stepsCreated}\n` +
2091
- `🖼️ Images Created: ${summary.phases.register.imagesCreated}\n` +
2092
- `📢 Published: ${summary.phases.publish.success ? 'Yes' : 'No'}\n\n` +
2093
- `⚡ Deployment is complete and active in the environment!`
2218
+ text: `Plugin deployment completed successfully!\n\n` +
2219
+ `Assembly: ${summary.phases.deploy.action === 'created' ? 'Created' : 'Updated'}\n` +
2220
+ `Assembly ID: ${summary.phases.deploy.assemblyId}\n` +
2221
+ `Version: ${summary.phases.deploy.version}\n` +
2222
+ `Size: ${(dllBuffer.length / 1024).toFixed(2)} KB\n` +
2223
+ (summary.phases.deploy.pluginTypes ? `Plugin Types: ${summary.phases.deploy.pluginTypes.length}\n` : '') +
2224
+ `Steps Created: ${summary.phases.register.stepsCreated}\n` +
2225
+ `Images Created: ${summary.phases.register.imagesCreated}\n` +
2226
+ `Published: ${summary.phases.publish.success ? 'Yes' : 'No'}\n\n` +
2227
+ `Deployment is complete and active in the environment!`
2094
2228
  }]
2095
2229
  };
2096
2230
  }
2097
2231
  catch (error) {
2232
+ // ROLLBACK: Clean up created resources on failure
2233
+ let rollbackMessage = '';
2234
+ if (createdStepIds.length > 0 || (createdAssemblyId && isNewAssembly)) {
2235
+ rollbackMessage = '\n\nRollback initiated:\n';
2236
+ // Delete created steps first (reverse order)
2237
+ for (const stepId of createdStepIds.reverse()) {
2238
+ try {
2239
+ await service.deletePluginStep(stepId);
2240
+ rollbackMessage += `- Deleted step: ${stepId}\n`;
2241
+ }
2242
+ catch (rollbackError) {
2243
+ rollbackMessage += `- Failed to delete step ${stepId}: ${rollbackError.message}\n`;
2244
+ }
2245
+ }
2246
+ // Delete the assembly if we created it (cascade deletes remaining components)
2247
+ if (createdAssemblyId && isNewAssembly) {
2248
+ try {
2249
+ await service.deletePluginAssembly(createdAssemblyId);
2250
+ rollbackMessage += `- Deleted assembly: ${createdAssemblyId}\n`;
2251
+ }
2252
+ catch (rollbackError) {
2253
+ rollbackMessage += `- Failed to delete assembly ${createdAssemblyId}: ${rollbackError.message}\n`;
2254
+ }
2255
+ }
2256
+ rollbackMessage += '\nPlease verify cleanup in Power Platform.';
2257
+ }
2098
2258
  console.error("Error deploying plugin:", error);
2099
2259
  return {
2100
- content: [{ type: "text", text: `❌ Failed to deploy plugin: ${error.message}` }],
2260
+ content: [{ type: "text", text: `Failed to deploy plugin: ${error.message}${rollbackMessage}` }],
2261
+ isError: true
2262
+ };
2263
+ }
2264
+ });
2265
+ server.tool("get-plugin-deployment-status", "Get the current deployment status of a plugin assembly, including all registered types, steps, and images. Useful for verifying deployments and troubleshooting.", {
2266
+ assemblyName: z.string().describe("Name of the plugin assembly to check"),
2267
+ includeDisabled: z.boolean().optional().describe("Include disabled steps (default: false)"),
2268
+ }, async (params) => {
2269
+ try {
2270
+ const service = getPowerPlatformService();
2271
+ // Check if assembly exists
2272
+ const assemblyId = await service.queryPluginAssemblyByName(params.assemblyName);
2273
+ if (!assemblyId) {
2274
+ return {
2275
+ content: [{
2276
+ type: "text",
2277
+ text: `Assembly '${params.assemblyName}' not found in Dataverse.\n\n` +
2278
+ `Possible reasons:\n` +
2279
+ `- Assembly has not been deployed yet\n` +
2280
+ `- Assembly name is incorrect (case-sensitive)\n` +
2281
+ `- Assembly was deleted\n\n` +
2282
+ `Use 'create-plugin-assembly' or 'deploy-plugin-complete' to deploy.`
2283
+ }]
2284
+ };
2285
+ }
2286
+ // Get complete assembly information
2287
+ const result = await service.getPluginAssemblyComplete(params.assemblyName, params.includeDisabled || false);
2288
+ // Build status report
2289
+ let statusReport = `PLUGIN DEPLOYMENT STATUS\n`;
2290
+ statusReport += `${'='.repeat(50)}\n\n`;
2291
+ // Assembly info
2292
+ statusReport += `ASSEMBLY\n`;
2293
+ statusReport += `---------\n`;
2294
+ statusReport += `Name: ${result.assembly.name}\n`;
2295
+ statusReport += `Version: ${result.assembly.version}\n`;
2296
+ statusReport += `ID: ${result.assembly.pluginassemblyid}\n`;
2297
+ statusReport += `Isolation Mode: ${result.assembly.isolationmode === 2 ? 'Sandbox' : 'None'}\n`;
2298
+ statusReport += `Is Managed: ${result.assembly.ismanaged ? 'Yes' : 'No'}\n`;
2299
+ statusReport += `Modified: ${result.assembly.modifiedon}\n`;
2300
+ statusReport += `Modified By: ${result.assembly.modifiedby?.fullname || 'Unknown'}\n\n`;
2301
+ // Plugin types
2302
+ statusReport += `PLUGIN TYPES (${result.pluginTypes.length})\n`;
2303
+ statusReport += `-------------\n`;
2304
+ if (result.pluginTypes.length === 0) {
2305
+ statusReport += `No plugin types found. This may indicate:\n`;
2306
+ statusReport += `- Dataverse is still processing the assembly\n`;
2307
+ statusReport += `- The DLL does not contain any IPlugin implementations\n\n`;
2308
+ }
2309
+ else {
2310
+ for (const type of result.pluginTypes) {
2311
+ statusReport += `- ${type.typename}\n`;
2312
+ statusReport += ` ID: ${type.plugintypeid}\n`;
2313
+ }
2314
+ statusReport += `\n`;
2315
+ }
2316
+ // Steps
2317
+ const stageNames = { 10: 'PreValidation', 20: 'PreOperation', 40: 'PostOperation' };
2318
+ const modeNames = { 0: 'Sync', 1: 'Async' };
2319
+ statusReport += `REGISTERED STEPS (${result.steps.length})\n`;
2320
+ statusReport += `------------------\n`;
2321
+ if (result.steps.length === 0) {
2322
+ statusReport += `No steps registered.\n\n`;
2323
+ }
2324
+ else {
2325
+ for (const step of result.steps) {
2326
+ const status = step.statuscode === 1 ? 'Active' : 'Disabled';
2327
+ statusReport += `- ${step.name}\n`;
2328
+ statusReport += ` Message: ${step.sdkmessageid?.name || 'Unknown'} on ${step.sdkmessagefilterid?.primaryobjecttypecode || 'Unknown'}\n`;
2329
+ statusReport += ` Stage: ${stageNames[step.stage] || step.stage}, Mode: ${modeNames[step.mode] || step.mode}\n`;
2330
+ statusReport += ` Status: ${status}, Rank: ${step.rank}\n`;
2331
+ statusReport += ` ID: ${step.sdkmessageprocessingstepid}\n`;
2332
+ if (step.images && step.images.length > 0) {
2333
+ statusReport += ` Images:\n`;
2334
+ for (const img of step.images) {
2335
+ const imgType = img.imagetype === 0 ? 'Pre' : img.imagetype === 1 ? 'Post' : 'Both';
2336
+ statusReport += ` - ${img.name} (${imgType}Image, alias: ${img.entityalias})\n`;
2337
+ }
2338
+ }
2339
+ statusReport += `\n`;
2340
+ }
2341
+ }
2342
+ // Validation
2343
+ statusReport += `VALIDATION\n`;
2344
+ statusReport += `----------\n`;
2345
+ if (result.validation.potentialIssues.length === 0) {
2346
+ statusReport += `No issues detected.\n`;
2347
+ }
2348
+ else {
2349
+ for (const issue of result.validation.potentialIssues) {
2350
+ statusReport += `Warning: ${issue}\n`;
2351
+ }
2352
+ }
2353
+ return {
2354
+ content: [{ type: "text", text: statusReport }]
2355
+ };
2356
+ }
2357
+ catch (error) {
2358
+ console.error("Error getting plugin deployment status:", error);
2359
+ return {
2360
+ content: [{ type: "text", text: `Failed to get plugin deployment status: ${error.message}` }],
2101
2361
  isError: true
2102
2362
  };
2103
2363
  }
2104
2364
  });
2105
- console.error(`✅ powerplatform-customization tools registered (${45} tools)`);
2365
+ console.error(`powerplatform-customization tools registered (${46} tools)`);
2106
2366
  }
2107
2367
  // CLI entry point (standalone execution)
2108
2368
  // Uses realpathSync to resolve symlinks created by npx