@mcp-consultant-tools/powerplatform-customization 2.0.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/build/index.js ADDED
@@ -0,0 +1,2171 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { createMcpServer, createEnvLoader } from '@mcp-consultant-tools/core';
5
+ import { PowerPlatformService } from './PowerPlatformService.js';
6
+ const POWERPLATFORM_DEFAULT_SOLUTION = process.env.POWERPLATFORM_DEFAULT_SOLUTION || "";
7
+ /**
8
+ * Register powerplatform-customization tools with an MCP server
9
+ * @param server - MCP server instance
10
+ * @param service - Optional pre-initialized PowerPlatformService (for testing)
11
+ */
12
+ export function registerPowerplatformCustomizationTools(server, service) {
13
+ // Check if customization is enabled
14
+ const customizationEnabled = process.env.POWERPLATFORM_ENABLE_CUSTOMIZATION === 'true';
15
+ if (!customizationEnabled) {
16
+ throw new Error('powerplatform-customization tools are disabled. Set POWERPLATFORM_ENABLE_CUSTOMIZATION=true to enable.');
17
+ }
18
+ let ppService = service || null;
19
+ // Check if customization is enabled
20
+ function checkCustomizationEnabled() {
21
+ if (process.env.POWERPLATFORM_ENABLE_CUSTOMIZATION !== 'true') {
22
+ throw new Error('Customization operations are disabled. Set POWERPLATFORM_ENABLE_CUSTOMIZATION=true to enable.');
23
+ }
24
+ }
25
+ function getPowerPlatformService() {
26
+ if (!ppService) {
27
+ const requiredVars = [
28
+ 'POWERPLATFORM_URL',
29
+ 'POWERPLATFORM_CLIENT_ID',
30
+ 'POWERPLATFORM_CLIENT_SECRET',
31
+ 'POWERPLATFORM_TENANT_ID'
32
+ ];
33
+ const missing = requiredVars.filter(v => !process.env[v]);
34
+ if (missing.length > 0) {
35
+ throw new Error(`Missing required PowerPlatform configuration: ${missing.join(', ')}`);
36
+ }
37
+ const config = {
38
+ organizationUrl: process.env.POWERPLATFORM_URL,
39
+ clientId: process.env.POWERPLATFORM_CLIENT_ID,
40
+ clientSecret: process.env.POWERPLATFORM_CLIENT_SECRET,
41
+ tenantId: process.env.POWERPLATFORM_TENANT_ID,
42
+ };
43
+ ppService = new PowerPlatformService(config);
44
+ }
45
+ return ppService;
46
+ }
47
+ // Tool registrations
48
+ server.tool("add-entities-to-app", "Add entities to a model-driven app (automatically adds them to navigation)", {
49
+ appId: z.string().describe("The GUID of the app (appmoduleid)"),
50
+ entityNames: z.array(z.string()).describe("Array of entity logical names to add (e.g., ['account', 'contact'])"),
51
+ }, async ({ appId, entityNames }) => {
52
+ try {
53
+ const service = getPowerPlatformService();
54
+ const result = await service.addEntitiesToApp(appId, entityNames);
55
+ const resultStr = JSON.stringify(result, null, 2);
56
+ return {
57
+ content: [
58
+ {
59
+ type: "text",
60
+ text: `Entities added successfully:\n\n${resultStr}`,
61
+ },
62
+ ],
63
+ };
64
+ }
65
+ catch (error) {
66
+ console.error("Error adding entities to app:", error);
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: `Failed to add entities to app: ${error.message}`,
72
+ },
73
+ ],
74
+ isError: true
75
+ };
76
+ }
77
+ });
78
+ server.tool("validate-app", "Validate a model-driven app before publishing (checks for missing components and configuration issues)", {
79
+ appId: z.string().describe("The GUID of the app (appmoduleid)"),
80
+ }, async ({ appId }) => {
81
+ try {
82
+ const service = getPowerPlatformService();
83
+ const result = await service.validateApp(appId);
84
+ const resultStr = JSON.stringify(result, null, 2);
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: `App validation result:\n\n${resultStr}`,
90
+ },
91
+ ],
92
+ };
93
+ }
94
+ catch (error) {
95
+ console.error("Error validating app:", error);
96
+ return {
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: `Failed to validate app: ${error.message}`,
101
+ },
102
+ ],
103
+ isError: true
104
+ };
105
+ }
106
+ });
107
+ server.tool("publish-app", "Publish a model-driven app to make it available to users (automatically validates first)", {
108
+ appId: z.string().describe("The GUID of the app (appmoduleid)"),
109
+ }, async ({ appId }) => {
110
+ try {
111
+ const service = getPowerPlatformService();
112
+ const result = await service.publishApp(appId);
113
+ const resultStr = JSON.stringify(result, null, 2);
114
+ return {
115
+ content: [
116
+ {
117
+ type: "text",
118
+ text: `App published successfully:\n\n${resultStr}`,
119
+ },
120
+ ],
121
+ };
122
+ }
123
+ catch (error) {
124
+ console.error("Error publishing app:", error);
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: `Failed to publish app: ${error.message}`,
130
+ },
131
+ ],
132
+ isError: true
133
+ };
134
+ }
135
+ });
136
+ server.tool("create-entity", "Create a new custom entity (table) in Dynamics 365 / PowerPlatform. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
137
+ schemaName: z.string().describe("The schema name of the entity (e.g., 'sic_application')"),
138
+ displayName: z.string().describe("The display name of the entity (e.g., 'Application')"),
139
+ pluralDisplayName: z.string().describe("The plural display name (e.g., 'Applications')"),
140
+ description: z.string().describe("Description of the entity"),
141
+ ownershipType: z.enum(["UserOwned", "TeamOwned", "OrganizationOwned"]).describe("Ownership type (default: UserOwned)"),
142
+ hasActivities: z.boolean().optional().describe("Enable activities (default: false)"),
143
+ hasNotes: z.boolean().optional().describe("Enable notes (default: false)"),
144
+ isActivityParty: z.boolean().optional().describe("Can be a party in activities (default: false)"),
145
+ primaryAttributeSchemaName: z.string().optional().describe("Schema name for primary attribute (default: 'name')"),
146
+ primaryAttributeDisplayName: z.string().optional().describe("Display name for primary attribute (default: 'Name')"),
147
+ primaryAttributeMaxLength: z.number().optional().describe("Max length for primary attribute (default: 850)"),
148
+ solutionUniqueName: z.string().optional().describe("Solution to add entity to (optional, uses POWERPLATFORM_DEFAULT_SOLUTION if not specified)")
149
+ }, async (params) => {
150
+ try {
151
+ checkCustomizationEnabled();
152
+ const service = getPowerPlatformService();
153
+ // Construct entity definition
154
+ const entityDefinition = {
155
+ "@odata.type": "Microsoft.Dynamics.CRM.EntityMetadata",
156
+ SchemaName: params.schemaName,
157
+ DisplayName: {
158
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
159
+ LocalizedLabels: [
160
+ {
161
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
162
+ Label: params.displayName,
163
+ LanguageCode: 1033
164
+ }
165
+ ]
166
+ },
167
+ DisplayCollectionName: {
168
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
169
+ LocalizedLabels: [
170
+ {
171
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
172
+ Label: params.pluralDisplayName,
173
+ LanguageCode: 1033
174
+ }
175
+ ]
176
+ },
177
+ Description: {
178
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
179
+ LocalizedLabels: [
180
+ {
181
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
182
+ Label: params.description,
183
+ LanguageCode: 1033
184
+ }
185
+ ]
186
+ },
187
+ OwnershipType: params.ownershipType,
188
+ IsActivity: false,
189
+ HasActivities: params.hasActivities || false,
190
+ HasNotes: params.hasNotes || false,
191
+ IsActivityParty: params.isActivityParty || false,
192
+ IsDuplicateDetectionEnabled: { Value: false, CanBeChanged: true },
193
+ IsMailMergeEnabled: { Value: false, CanBeChanged: true },
194
+ Attributes: [
195
+ {
196
+ "@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
197
+ SchemaName: params.primaryAttributeSchemaName || "name",
198
+ IsPrimaryName: true,
199
+ RequiredLevel: {
200
+ Value: "None",
201
+ CanBeChanged: true
202
+ },
203
+ MaxLength: params.primaryAttributeMaxLength || 850,
204
+ FormatName: {
205
+ Value: "Text"
206
+ },
207
+ DisplayName: {
208
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
209
+ LocalizedLabels: [
210
+ {
211
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
212
+ Label: params.primaryAttributeDisplayName || "Name",
213
+ LanguageCode: 1033
214
+ }
215
+ ]
216
+ },
217
+ Description: {
218
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
219
+ LocalizedLabels: [
220
+ {
221
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
222
+ Label: "The primary attribute for the entity",
223
+ LanguageCode: 1033
224
+ }
225
+ ]
226
+ }
227
+ }
228
+ ],
229
+ HasFeedback: false
230
+ };
231
+ const solutionName = params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION || undefined;
232
+ const result = await service.createEntity(entityDefinition, solutionName);
233
+ return {
234
+ content: [
235
+ {
236
+ type: "text",
237
+ text: `Successfully created entity '${params.schemaName}'.\n\n` +
238
+ `Details:\n${JSON.stringify(result, null, 2)}\n\n` +
239
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
240
+ }
241
+ ]
242
+ };
243
+ }
244
+ catch (error) {
245
+ console.error("Error creating entity:", error);
246
+ return {
247
+ content: [
248
+ {
249
+ type: "text",
250
+ text: `Failed to create entity: ${error.message}`
251
+ }
252
+ ],
253
+ isError: true
254
+ };
255
+ }
256
+ });
257
+ server.tool("update-entity", "Update an existing custom entity. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
258
+ metadataId: z.string().describe("The MetadataId of the entity (GUID)"),
259
+ displayName: z.string().optional().describe("New display name"),
260
+ pluralDisplayName: z.string().optional().describe("New plural display name"),
261
+ description: z.string().optional().describe("New description"),
262
+ hasActivities: z.boolean().optional().describe("Enable/disable activities"),
263
+ hasNotes: z.boolean().optional().describe("Enable/disable notes"),
264
+ solutionUniqueName: z.string().optional().describe("Solution context")
265
+ }, async (params) => {
266
+ try {
267
+ checkCustomizationEnabled();
268
+ const service = getPowerPlatformService();
269
+ const updates = {};
270
+ if (params.displayName) {
271
+ updates.DisplayName = {
272
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
273
+ LocalizedLabels: [{ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", Label: params.displayName, LanguageCode: 1033 }]
274
+ };
275
+ }
276
+ if (params.pluralDisplayName) {
277
+ updates.DisplayCollectionName = {
278
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
279
+ LocalizedLabels: [{ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", Label: params.pluralDisplayName, LanguageCode: 1033 }]
280
+ };
281
+ }
282
+ if (params.description) {
283
+ updates.Description = {
284
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
285
+ LocalizedLabels: [{ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", Label: params.description, LanguageCode: 1033 }]
286
+ };
287
+ }
288
+ if (params.hasActivities !== undefined)
289
+ updates.HasActivities = params.hasActivities;
290
+ if (params.hasNotes !== undefined)
291
+ updates.HasNotes = params.hasNotes;
292
+ await service.updateEntity(params.metadataId, updates, params.solutionUniqueName);
293
+ return {
294
+ content: [{ type: "text", text: `✅ Successfully updated entity (${params.metadataId})\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.` }]
295
+ };
296
+ }
297
+ catch (error) {
298
+ console.error("Error updating entity:", error);
299
+ return { content: [{ type: "text", text: `Failed to update entity: ${error.message}` }], isError: true };
300
+ }
301
+ });
302
+ server.tool("update-entity-icon", "Update entity icon using Fluent UI System Icons from Microsoft's official icon library. Creates a web resource and sets it as the entity icon. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
303
+ entityLogicalName: z.string().describe("The logical name of the entity (e.g., 'sic_strikeaction')"),
304
+ iconFileName: z.string().describe("Fluent UI icon file name (e.g., 'people_community_24_filled.svg'). Browse icons at: https://github.com/microsoft/fluentui-system-icons"),
305
+ solutionUniqueName: z.string().optional().describe("Solution to add the web resource to (optional, uses POWERPLATFORM_DEFAULT_SOLUTION if not specified)")
306
+ }, async (params) => {
307
+ try {
308
+ checkCustomizationEnabled();
309
+ const service = getPowerPlatformService();
310
+ const result = await service.updateEntityIcon(params.entityLogicalName, params.iconFileName, params.solutionUniqueName);
311
+ const message = `✅ Successfully updated entity icon
312
+
313
+ **Entity:** ${result.entityLogicalName} (${result.entitySchemaName})
314
+ **Icon:** ${result.iconFileName}
315
+ **Web Resource:** ${result.webResourceName}
316
+ **Web Resource ID:** ${result.webResourceId}
317
+ **Icon Vector Name:** ${result.iconVectorName}
318
+
319
+ ✨ **Published:** The icon has been automatically published and should now be visible in the UI.
320
+
321
+ 💡 TIP: Browse available Fluent UI icons at https://github.com/microsoft/fluentui-system-icons`;
322
+ return {
323
+ content: [{ type: "text", text: message }]
324
+ };
325
+ }
326
+ catch (error) {
327
+ console.error("Error updating entity icon:", error);
328
+ return {
329
+ content: [{
330
+ type: "text",
331
+ text: `❌ Failed to update entity icon: ${error.message}\n\n💡 Make sure the icon file name is valid (e.g., 'people_community_24_filled.svg'). Browse available icons at https://github.com/microsoft/fluentui-system-icons`
332
+ }],
333
+ isError: true
334
+ };
335
+ }
336
+ });
337
+ server.tool("delete-entity", "Delete a custom entity. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
338
+ metadataId: z.string().describe("The MetadataId of the entity to delete (GUID)")
339
+ }, async ({ metadataId }) => {
340
+ try {
341
+ checkCustomizationEnabled();
342
+ const service = getPowerPlatformService();
343
+ await service.deleteEntity(metadataId);
344
+ return {
345
+ content: [{ type: "text", text: `✅ Successfully deleted entity (${metadataId})\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.` }]
346
+ };
347
+ }
348
+ catch (error) {
349
+ console.error("Error deleting entity:", error);
350
+ return { content: [{ type: "text", text: `Failed to delete entity: ${error.message}` }], isError: true };
351
+ }
352
+ });
353
+ server.tool("create-attribute", "Create a new attribute (column) on a Dynamics 365 entity. Supports most attribute types. CRITICAL LIMITATIONS: (1) Local option sets are NOT SUPPORTED - all Picklist/MultiSelectPicklist attributes MUST use global option sets. Provide 'optionSetOptions' to auto-create a new global option set, or 'globalOptionSetName' to reference existing. (2) Customer-type attributes (polymorphic lookups) CANNOT be created via SDK - use a standard Lookup to Account or Contact instead, or create manually via Power Apps maker portal. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
354
+ entityLogicalName: z.string().describe("The logical name of the entity"),
355
+ attributeType: z.enum([
356
+ "String", "Memo", "Integer", "Decimal", "Money", "DateTime",
357
+ "Boolean", "Picklist", "Lookup", "Customer", "MultiSelectPicklist", "AutoNumber"
358
+ ]).describe("The type of attribute to create"),
359
+ schemaName: z.string().describe("The schema name of the attribute (e.g., 'sic_description')"),
360
+ displayName: z.string().describe("The display name of the attribute"),
361
+ description: z.string().optional().describe("Description of the attribute"),
362
+ isRequired: z.boolean().optional().describe("Whether the attribute is required (default: false)"),
363
+ // String-specific
364
+ maxLength: z.number().optional().describe("Max length (for String/Memo attributes)"),
365
+ // AutoNumber-specific
366
+ autoNumberFormat: z.string().optional().describe("Auto-number format string (for AutoNumber type). " +
367
+ "Use placeholders: {SEQNUM:n} for sequential number (min length n), " +
368
+ "{RANDSTRING:n} for random alphanumeric (length 1-6 only), " +
369
+ "{DATETIMEUTC:format} for UTC timestamp (.NET format). " +
370
+ "Example: 'AUTO-{SEQNUM:5}-{RANDSTRING:4}' produces AUTO-00001-A7K2, AUTO-00002-B9M4, etc."),
371
+ // Decimal/Money-specific
372
+ precision: z.number().optional().describe("Precision (for Decimal/Money attributes)"),
373
+ minValue: z.number().optional().describe("Minimum value (for Integer/Decimal/Money attributes)"),
374
+ maxValue: z.number().optional().describe("Maximum value (for Integer/Decimal/Money attributes)"),
375
+ // DateTime-specific
376
+ dateTimeBehavior: z.enum(["UserLocal", "DateOnly", "TimeZoneIndependent"]).optional().describe("DateTime behavior"),
377
+ // Picklist-specific
378
+ globalOptionSetName: z.string().optional().describe("Name of existing global option set to use (for Picklist/MultiSelectPicklist). If not provided and optionSetOptions is given, a new global option set will be created automatically."),
379
+ optionSetOptions: z.union([
380
+ z.array(z.string()),
381
+ z.array(z.object({
382
+ value: z.number(),
383
+ label: z.string()
384
+ }))
385
+ ]).optional().describe("Options for new global option set. Can be either: 1) Array of strings (values auto-numbered 0,1,2...) RECOMMENDED, or 2) Array of {value, label} objects for custom values. A global option set will be created automatically with the name matching the attribute SchemaName."),
386
+ // Lookup-specific
387
+ referencedEntity: z.string().optional().describe("Referenced entity logical name (for Lookup attributes)"),
388
+ relationshipSchemaName: z.string().optional().describe("Schema name for the relationship (for Lookup attributes)"),
389
+ solutionUniqueName: z.string().optional().describe("Solution to add attribute to")
390
+ }, async (params) => {
391
+ try {
392
+ checkCustomizationEnabled();
393
+ const service = getPowerPlatformService();
394
+ // Validate Customer attribute type early with helpful error
395
+ if (params.attributeType === "Customer") {
396
+ throw new Error("Customer-type attributes cannot be created via the PowerPlatform SDK.\n\n" +
397
+ "🔴 MICROSOFT LIMITATION: The Dataverse Web API does not support programmatic creation of Customer (polymorphic lookup) attributes.\n\n" +
398
+ "✅ WORKAROUNDS:\n" +
399
+ "1. Create manually via Power Apps maker portal (make.powerapps.com)\n" +
400
+ "2. Use a standard Lookup to a specific entity:\n" +
401
+ " - For Account: Set attributeType='Lookup' and referencedEntity='account'\n" +
402
+ " - For Contact: Set attributeType='Lookup' and referencedEntity='contact'\n" +
403
+ "3. Create separate lookup fields:\n" +
404
+ " - " + params.schemaName + "_account (Lookup to Account)\n" +
405
+ " - " + params.schemaName + "_contact (Lookup to Contact)\n" +
406
+ " - Use business logic to ensure only one is populated\n\n" +
407
+ "For more information, see Microsoft's documentation on Customer attributes.");
408
+ }
409
+ // Build base attribute definition
410
+ const baseDefinition = {
411
+ SchemaName: params.schemaName,
412
+ RequiredLevel: {
413
+ Value: params.isRequired ? "ApplicationRequired" : "None",
414
+ CanBeChanged: true
415
+ },
416
+ DisplayName: {
417
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
418
+ LocalizedLabels: [
419
+ {
420
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
421
+ Label: params.displayName,
422
+ LanguageCode: 1033
423
+ }
424
+ ]
425
+ },
426
+ Description: {
427
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
428
+ LocalizedLabels: [
429
+ {
430
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
431
+ Label: params.description || "",
432
+ LanguageCode: 1033
433
+ }
434
+ ]
435
+ }
436
+ };
437
+ let attributeDefinition;
438
+ // Build type-specific definition
439
+ switch (params.attributeType) {
440
+ case "String":
441
+ attributeDefinition = {
442
+ ...baseDefinition,
443
+ "@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
444
+ MaxLength: params.maxLength || 100,
445
+ FormatName: { Value: "Text" }
446
+ };
447
+ break;
448
+ case "AutoNumber":
449
+ if (!params.autoNumberFormat) {
450
+ throw new Error("AutoNumber attributes require an 'autoNumberFormat' parameter.\n\n" +
451
+ "Format placeholders:\n" +
452
+ " {SEQNUM:n} - Sequential number (min length n, grows as needed)\n" +
453
+ " {RANDSTRING:n} - Random alphanumeric string (length 1-6 ONLY)\n" +
454
+ " {DATETIMEUTC:fmt} - UTC timestamp with .NET format\n\n" +
455
+ "Examples:\n" +
456
+ " 'AUTO-{SEQNUM:5}' → AUTO-00001, AUTO-00002...\n" +
457
+ " 'CASE-{SEQNUM:4}-{DATETIMEUTC:yyyyMMdd}' → CASE-0001-20250115\n" +
458
+ " 'WID-{SEQNUM:3}-{RANDSTRING:6}' → WID-001-A7K2M9\n\n" +
459
+ "Note: RANDSTRING length must be 1-6 (API limitation)");
460
+ }
461
+ // Validate RANDSTRING lengths (common error - API rejects length > 6)
462
+ const randstringMatches = params.autoNumberFormat.match(/\{RANDSTRING:(\d+)\}/gi);
463
+ if (randstringMatches) {
464
+ for (const match of randstringMatches) {
465
+ const lengthMatch = match.match(/\{RANDSTRING:(\d+)\}/i);
466
+ if (lengthMatch) {
467
+ const length = parseInt(lengthMatch[1]);
468
+ if (length < 1 || length > 6) {
469
+ throw new Error(`Invalid RANDSTRING length: ${length}\n\n` +
470
+ "RANDSTRING must be between 1-6 characters (Dataverse API limitation).\n" +
471
+ `Found in format: ${params.autoNumberFormat}\n\n` +
472
+ `Please change {RANDSTRING:${length}} to {RANDSTRING:6} or less.`);
473
+ }
474
+ }
475
+ }
476
+ }
477
+ attributeDefinition = {
478
+ ...baseDefinition,
479
+ "@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
480
+ AutoNumberFormat: params.autoNumberFormat,
481
+ MaxLength: params.maxLength || 100, // Default to 100, user can override
482
+ FormatName: { Value: "Text" } // MUST be Text for auto-number
483
+ };
484
+ break;
485
+ case "Memo":
486
+ attributeDefinition = {
487
+ ...baseDefinition,
488
+ "@odata.type": "Microsoft.Dynamics.CRM.MemoAttributeMetadata",
489
+ MaxLength: params.maxLength || 2000,
490
+ Format: "TextArea"
491
+ };
492
+ break;
493
+ case "Integer":
494
+ attributeDefinition = {
495
+ ...baseDefinition,
496
+ "@odata.type": "Microsoft.Dynamics.CRM.IntegerAttributeMetadata",
497
+ Format: "None",
498
+ MinValue: params.minValue ?? -2147483648,
499
+ MaxValue: params.maxValue ?? 2147483647
500
+ };
501
+ break;
502
+ case "Decimal":
503
+ attributeDefinition = {
504
+ ...baseDefinition,
505
+ "@odata.type": "Microsoft.Dynamics.CRM.DecimalAttributeMetadata",
506
+ Precision: params.precision || 2,
507
+ MinValue: params.minValue ?? -100000000000,
508
+ MaxValue: params.maxValue ?? 100000000000
509
+ };
510
+ break;
511
+ case "Money":
512
+ attributeDefinition = {
513
+ ...baseDefinition,
514
+ "@odata.type": "Microsoft.Dynamics.CRM.MoneyAttributeMetadata",
515
+ Precision: params.precision || 2,
516
+ MinValue: params.minValue ?? -922337203685477,
517
+ MaxValue: params.maxValue ?? 922337203685477,
518
+ PrecisionSource: 2
519
+ };
520
+ break;
521
+ case "DateTime":
522
+ attributeDefinition = {
523
+ ...baseDefinition,
524
+ "@odata.type": "Microsoft.Dynamics.CRM.DateTimeAttributeMetadata",
525
+ Format: params.dateTimeBehavior === "DateOnly" ? "DateOnly" : "DateAndTime",
526
+ DateTimeBehavior: {
527
+ Value: params.dateTimeBehavior || "UserLocal"
528
+ }
529
+ };
530
+ break;
531
+ case "Boolean":
532
+ attributeDefinition = {
533
+ ...baseDefinition,
534
+ "@odata.type": "Microsoft.Dynamics.CRM.BooleanAttributeMetadata",
535
+ DefaultValue: false,
536
+ OptionSet: {
537
+ "@odata.type": "Microsoft.Dynamics.CRM.BooleanOptionSetMetadata",
538
+ TrueOption: {
539
+ Value: 1,
540
+ Label: {
541
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
542
+ LocalizedLabels: [
543
+ {
544
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
545
+ Label: "Yes",
546
+ LanguageCode: 1033
547
+ }
548
+ ]
549
+ }
550
+ },
551
+ FalseOption: {
552
+ Value: 0,
553
+ Label: {
554
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
555
+ LocalizedLabels: [
556
+ {
557
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
558
+ Label: "No",
559
+ LanguageCode: 1033
560
+ }
561
+ ]
562
+ }
563
+ }
564
+ }
565
+ };
566
+ break;
567
+ case "Picklist":
568
+ // ALWAYS use global option sets
569
+ if (params.globalOptionSetName) {
570
+ // Using existing global option set - need to look up its MetadataId first
571
+ const globalOptionSet = await service.getGlobalOptionSet(params.globalOptionSetName);
572
+ const metadataId = globalOptionSet.MetadataId;
573
+ attributeDefinition = {
574
+ ...baseDefinition,
575
+ "@odata.type": "Microsoft.Dynamics.CRM.PicklistAttributeMetadata",
576
+ "GlobalOptionSet@odata.bind": `/GlobalOptionSetDefinitions(${metadataId})`
577
+ };
578
+ }
579
+ else if (params.optionSetOptions && params.optionSetOptions.length > 0) {
580
+ // Create NEW global option set in TWO steps:
581
+ // Step 1: Create the global option set separately
582
+ // Step 2: Create the attribute that references it
583
+ const optionSetName = params.schemaName;
584
+ // Normalize options: support both string[] (auto-numbered) and {value, label}[] formats
585
+ const normalizedOptions = params.optionSetOptions.map((opt, index) => {
586
+ if (typeof opt === 'string') {
587
+ // Auto-number from 0
588
+ return {
589
+ Value: index,
590
+ Label: {
591
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
592
+ LocalizedLabels: [
593
+ {
594
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
595
+ Label: opt,
596
+ LanguageCode: 1033
597
+ }
598
+ ]
599
+ }
600
+ };
601
+ }
602
+ else {
603
+ // User provided explicit value
604
+ return {
605
+ Value: opt.value,
606
+ Label: {
607
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
608
+ LocalizedLabels: [
609
+ {
610
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
611
+ Label: opt.label,
612
+ LanguageCode: 1033
613
+ }
614
+ ]
615
+ }
616
+ };
617
+ }
618
+ });
619
+ // Step 1: Create the global option set first
620
+ const globalOptionSetDefinition = {
621
+ "@odata.type": "Microsoft.Dynamics.CRM.OptionSetMetadata",
622
+ Name: optionSetName,
623
+ DisplayName: baseDefinition.DisplayName,
624
+ Description: baseDefinition.Description,
625
+ IsGlobal: true,
626
+ OptionSetType: "Picklist",
627
+ Options: normalizedOptions
628
+ };
629
+ // Store this for later - we'll create it before the attribute
630
+ baseDefinition._createGlobalOptionSetFirst = globalOptionSetDefinition;
631
+ baseDefinition._globalOptionSetNameToLookup = optionSetName;
632
+ // Step 2: Create attribute definition that REFERENCES the global option set
633
+ // The MetadataId binding will be set after creating the global option set
634
+ attributeDefinition = {
635
+ ...baseDefinition,
636
+ "@odata.type": "Microsoft.Dynamics.CRM.PicklistAttributeMetadata"
637
+ };
638
+ }
639
+ else {
640
+ throw new Error("For Picklist attributes, you must provide either:\n" +
641
+ "1. 'globalOptionSetName' to reference an existing global option set, OR\n" +
642
+ "2. 'optionSetOptions' to create a new global option set automatically\n\n" +
643
+ "Note: Local option sets are not supported - all option sets are created as global for consistency and reusability.");
644
+ }
645
+ break;
646
+ case "Lookup":
647
+ if (!params.referencedEntity) {
648
+ throw new Error("referencedEntity is required for Lookup attributes");
649
+ }
650
+ attributeDefinition = {
651
+ ...baseDefinition,
652
+ "@odata.type": "Microsoft.Dynamics.CRM.LookupAttributeMetadata",
653
+ Targets: [params.referencedEntity]
654
+ };
655
+ // For lookups, we also need relationship information
656
+ if (params.relationshipSchemaName) {
657
+ attributeDefinition.RelationshipSchemaName = params.relationshipSchemaName;
658
+ }
659
+ break;
660
+ case "MultiSelectPicklist":
661
+ // ALWAYS use global option sets
662
+ if (params.globalOptionSetName) {
663
+ // Using existing global option set - need to look up its MetadataId first
664
+ const globalOptionSet = await service.getGlobalOptionSet(params.globalOptionSetName);
665
+ const metadataId = globalOptionSet.MetadataId;
666
+ attributeDefinition = {
667
+ ...baseDefinition,
668
+ "@odata.type": "Microsoft.Dynamics.CRM.MultiSelectPicklistAttributeMetadata",
669
+ "GlobalOptionSet@odata.bind": `/GlobalOptionSetDefinitions(${metadataId})`
670
+ };
671
+ }
672
+ else if (params.optionSetOptions && params.optionSetOptions.length > 0) {
673
+ // Create NEW global option set in TWO steps:
674
+ // Step 1: Create the global option set separately
675
+ // Step 2: Create the attribute that references it
676
+ const optionSetName = params.schemaName;
677
+ // Normalize options: support both string[] (auto-numbered) and {value, label}[] formats
678
+ const normalizedOptions = params.optionSetOptions.map((opt, index) => {
679
+ if (typeof opt === 'string') {
680
+ // Auto-number from 0
681
+ return {
682
+ Value: index,
683
+ Label: {
684
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
685
+ LocalizedLabels: [
686
+ {
687
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
688
+ Label: opt,
689
+ LanguageCode: 1033
690
+ }
691
+ ]
692
+ }
693
+ };
694
+ }
695
+ else {
696
+ // User provided explicit value
697
+ return {
698
+ Value: opt.value,
699
+ Label: {
700
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
701
+ LocalizedLabels: [
702
+ {
703
+ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
704
+ Label: opt.label,
705
+ LanguageCode: 1033
706
+ }
707
+ ]
708
+ }
709
+ };
710
+ }
711
+ });
712
+ // Step 1: Create the global option set first
713
+ const globalOptionSetDefinition = {
714
+ "@odata.type": "Microsoft.Dynamics.CRM.OptionSetMetadata",
715
+ Name: optionSetName,
716
+ DisplayName: baseDefinition.DisplayName,
717
+ Description: baseDefinition.Description,
718
+ IsGlobal: true,
719
+ OptionSetType: "Picklist",
720
+ Options: normalizedOptions
721
+ };
722
+ // Store this for later - we'll create it before the attribute
723
+ baseDefinition._createGlobalOptionSetFirst = globalOptionSetDefinition;
724
+ baseDefinition._globalOptionSetNameToLookup = optionSetName;
725
+ // Step 2: Create attribute definition that REFERENCES the global option set
726
+ // The MetadataId binding will be set after creating the global option set
727
+ attributeDefinition = {
728
+ ...baseDefinition,
729
+ "@odata.type": "Microsoft.Dynamics.CRM.MultiSelectPicklistAttributeMetadata"
730
+ };
731
+ }
732
+ else {
733
+ throw new Error("For MultiSelectPicklist attributes, you must provide either:\n" +
734
+ "1. 'globalOptionSetName' to reference an existing global option set, OR\n" +
735
+ "2. 'optionSetOptions' to create a new global option set automatically\n\n" +
736
+ "Note: Local option sets are not supported - all option sets are created as global for consistency and reusability.");
737
+ }
738
+ break;
739
+ default:
740
+ throw new Error(`Attribute type '${params.attributeType}' is not yet fully implemented. Contact support.`);
741
+ }
742
+ const solutionName = params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION || undefined;
743
+ // Check if we need to create a global option set first (two-step process)
744
+ if (attributeDefinition._createGlobalOptionSetFirst) {
745
+ const globalOptionSetDef = attributeDefinition._createGlobalOptionSetFirst;
746
+ const optionSetNameToLookup = attributeDefinition._globalOptionSetNameToLookup;
747
+ delete attributeDefinition._createGlobalOptionSetFirst; // Clean up marker
748
+ delete attributeDefinition._globalOptionSetNameToLookup;
749
+ // Step 1: Create the global option set
750
+ await service.createGlobalOptionSet(globalOptionSetDef, solutionName);
751
+ // Step 1.5: Look up the created global option set to get its MetadataId
752
+ const createdGlobalOptionSet = await service.getGlobalOptionSet(optionSetNameToLookup);
753
+ const metadataId = createdGlobalOptionSet.MetadataId;
754
+ // Add the binding to the attribute definition
755
+ attributeDefinition["GlobalOptionSet@odata.bind"] = `/GlobalOptionSetDefinitions(${metadataId})`;
756
+ }
757
+ // Step 2: Create the attribute (which now references the global option set)
758
+ const result = await service.createAttribute(params.entityLogicalName, attributeDefinition, solutionName);
759
+ return {
760
+ content: [
761
+ {
762
+ type: "text",
763
+ text: `Successfully created ${params.attributeType} attribute '${params.schemaName}' on entity '${params.entityLogicalName}'.\n\n` +
764
+ (params.attributeType === "AutoNumber" && params.autoNumberFormat ? `Auto-number format: ${params.autoNumberFormat}\n\n` : "") +
765
+ `Details:\n${JSON.stringify(result, null, 2)}\n\n` +
766
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
767
+ }
768
+ ]
769
+ };
770
+ }
771
+ catch (error) {
772
+ console.error("Error creating attribute:", error);
773
+ // Provide helpful guidance for common errors
774
+ let errorMessage = error.message;
775
+ let helpfulGuidance = "";
776
+ // Detect global option set errors
777
+ if (errorMessage.includes("IsGlobal") || errorMessage.includes("0x80048403")) {
778
+ helpfulGuidance = "\n\n🔴 ERROR EXPLANATION: An error occurred while creating the global option set.\n\n" +
779
+ "✅ SOLUTION: This tool creates global option sets in a two-step process:\n" +
780
+ "1. First, it creates the global option set\n" +
781
+ "2. Then, it creates the attribute that references it\n\n" +
782
+ "This error may mean:\n" +
783
+ "- A global option set with name '" + params.schemaName + "' already exists\n" +
784
+ "- There was an issue with the option set definition\n\n" +
785
+ "Try using a different schema name or reference the existing global option set:\n" +
786
+ "{\n" +
787
+ " entityLogicalName: \"" + params.entityLogicalName + "\",\n" +
788
+ " attributeType: \"" + params.attributeType + "\",\n" +
789
+ " schemaName: \"" + params.schemaName + "\",\n" +
790
+ " displayName: \"" + params.displayName + "\",\n" +
791
+ " globalOptionSetName: \"existing_option_set_name\"\n" +
792
+ "}";
793
+ }
794
+ return {
795
+ content: [
796
+ {
797
+ type: "text",
798
+ text: `Failed to create attribute: ${errorMessage}${helpfulGuidance}`
799
+ }
800
+ ],
801
+ isError: true
802
+ };
803
+ }
804
+ });
805
+ server.tool("update-attribute", "Update an existing attribute on an entity. Supports converting String attributes to AutoNumber by setting autoNumberFormat. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
806
+ entityLogicalName: z.string().describe("Entity logical name"),
807
+ attributeLogicalName: z.string().describe("Attribute logical name"),
808
+ displayName: z.string().optional().describe("New display name"),
809
+ description: z.string().optional().describe("New description"),
810
+ requiredLevel: z.enum(["None", "Recommended", "ApplicationRequired"]).optional().describe("Required level"),
811
+ autoNumberFormat: z.string().optional().describe("Auto-number format string to convert String attribute to AutoNumber. " +
812
+ "Use placeholders: {SEQNUM:n} for sequential number (min length n), " +
813
+ "{RANDSTRING:n} for random alphanumeric (length 1-6 only), " +
814
+ "{DATETIMEUTC:format} for UTC timestamp (.NET format). " +
815
+ "Example: 'AUTO-{SEQNUM:5}-{RANDSTRING:4}' produces AUTO-00001-A7K2, AUTO-00002-B9M4, etc."),
816
+ solutionUniqueName: z.string().optional().describe("Solution context")
817
+ }, async (params) => {
818
+ try {
819
+ checkCustomizationEnabled();
820
+ const service = getPowerPlatformService();
821
+ const updates = {};
822
+ if (params.displayName) {
823
+ updates.DisplayName = {
824
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
825
+ LocalizedLabels: [{ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", Label: params.displayName, LanguageCode: 1033 }]
826
+ };
827
+ }
828
+ if (params.description) {
829
+ updates.Description = {
830
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
831
+ LocalizedLabels: [{ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", Label: params.description, LanguageCode: 1033 }]
832
+ };
833
+ }
834
+ if (params.requiredLevel) {
835
+ updates.RequiredLevel = { Value: params.requiredLevel, CanBeChanged: true };
836
+ }
837
+ // Handle AutoNumber format conversion
838
+ if (params.autoNumberFormat) {
839
+ // Validate RANDSTRING lengths (common error - API rejects length > 6)
840
+ const randstringMatches = params.autoNumberFormat.match(/\{RANDSTRING:(\d+)\}/gi);
841
+ if (randstringMatches) {
842
+ for (const match of randstringMatches) {
843
+ const lengthMatch = match.match(/\{RANDSTRING:(\d+)\}/i);
844
+ if (lengthMatch) {
845
+ const length = parseInt(lengthMatch[1]);
846
+ if (length < 1 || length > 6) {
847
+ throw new Error(`Invalid RANDSTRING length: ${length}\n\n` +
848
+ "RANDSTRING must be between 1-6 characters (Dataverse API limitation).\n" +
849
+ `Found in format: ${params.autoNumberFormat}\n\n` +
850
+ `Please change {RANDSTRING:${length}} to {RANDSTRING:6} or less.`);
851
+ }
852
+ }
853
+ }
854
+ }
855
+ updates.AutoNumberFormat = params.autoNumberFormat;
856
+ }
857
+ await service.updateAttribute(params.entityLogicalName, params.attributeLogicalName, updates, params.solutionUniqueName);
858
+ let successMessage = `✅ Successfully updated attribute '${params.attributeLogicalName}' on entity '${params.entityLogicalName}'`;
859
+ if (params.autoNumberFormat) {
860
+ successMessage += `\n\n📋 Auto-number format set to: ${params.autoNumberFormat}`;
861
+ successMessage += `\n\n⚠️ NOTE: Converting to AutoNumber is irreversible. The attribute will now auto-generate values based on the format.`;
862
+ }
863
+ successMessage += `\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`;
864
+ return {
865
+ content: [{ type: "text", text: successMessage }]
866
+ };
867
+ }
868
+ catch (error) {
869
+ console.error("Error updating attribute:", error);
870
+ return { content: [{ type: "text", text: `Failed to update attribute: ${error.message}` }], isError: true };
871
+ }
872
+ });
873
+ server.tool("delete-attribute", "Delete an attribute from an entity. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
874
+ entityLogicalName: z.string().describe("Entity logical name"),
875
+ attributeMetadataId: z.string().describe("Attribute MetadataId (GUID)")
876
+ }, async ({ entityLogicalName, attributeMetadataId }) => {
877
+ try {
878
+ checkCustomizationEnabled();
879
+ const service = getPowerPlatformService();
880
+ await service.deleteAttribute(entityLogicalName, attributeMetadataId);
881
+ return {
882
+ content: [{ type: "text", text: `✅ Successfully deleted attribute (${attributeMetadataId}) from entity '${entityLogicalName}'\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.` }]
883
+ };
884
+ }
885
+ catch (error) {
886
+ console.error("Error deleting attribute:", error);
887
+ return { content: [{ type: "text", text: `Failed to delete attribute: ${error.message}` }], isError: true };
888
+ }
889
+ });
890
+ server.tool("create-one-to-many-relationship", "Create a one-to-many relationship between two entities. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
891
+ referencedEntity: z.string().describe("The 'one' side entity (parent)"),
892
+ referencingEntity: z.string().describe("The 'many' side entity (child)"),
893
+ schemaName: z.string().describe("Relationship schema name (e.g., 'sic_account_application')"),
894
+ lookupAttributeSchemaName: z.string().describe("Lookup attribute schema name (e.g., 'sic_accountid')"),
895
+ lookupAttributeDisplayName: z.string().describe("Lookup attribute display name"),
896
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
897
+ }, async (params) => {
898
+ try {
899
+ checkCustomizationEnabled();
900
+ const service = getPowerPlatformService();
901
+ const relationshipDefinition = {
902
+ "@odata.type": "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata",
903
+ SchemaName: params.schemaName,
904
+ ReferencedEntity: params.referencedEntity,
905
+ ReferencingEntity: params.referencingEntity,
906
+ Lookup: {
907
+ "@odata.type": "Microsoft.Dynamics.CRM.LookupAttributeMetadata",
908
+ SchemaName: params.lookupAttributeSchemaName,
909
+ DisplayName: {
910
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
911
+ LocalizedLabels: [{ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", Label: params.lookupAttributeDisplayName, LanguageCode: 1033 }]
912
+ }
913
+ }
914
+ };
915
+ const solution = params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
916
+ await service.createOneToManyRelationship(relationshipDefinition, solution);
917
+ return {
918
+ content: [{ type: "text", text: `✅ Successfully created 1:N relationship '${params.schemaName}'\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.` }]
919
+ };
920
+ }
921
+ catch (error) {
922
+ console.error("Error creating relationship:", error);
923
+ return { content: [{ type: "text", text: `Failed to create relationship: ${error.message}` }], isError: true };
924
+ }
925
+ });
926
+ server.tool("create-many-to-many-relationship", "Create a many-to-many relationship between two entities. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
927
+ entity1: z.string().describe("First entity logical name"),
928
+ entity2: z.string().describe("Second entity logical name"),
929
+ schemaName: z.string().describe("Relationship schema name (e.g., 'sic_account_contact')"),
930
+ intersectEntityName: z.string().describe("Intersect entity name (e.g., 'sic_account_contact')"),
931
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
932
+ }, async (params) => {
933
+ try {
934
+ checkCustomizationEnabled();
935
+ const service = getPowerPlatformService();
936
+ const relationshipDefinition = {
937
+ "@odata.type": "Microsoft.Dynamics.CRM.ManyToManyRelationshipMetadata",
938
+ SchemaName: params.schemaName,
939
+ Entity1LogicalName: params.entity1,
940
+ Entity2LogicalName: params.entity2,
941
+ IntersectEntityName: params.intersectEntityName
942
+ };
943
+ const solution = params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
944
+ await service.createManyToManyRelationship(relationshipDefinition, solution);
945
+ return {
946
+ content: [{ type: "text", text: `✅ Successfully created N:N relationship '${params.schemaName}'\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.` }]
947
+ };
948
+ }
949
+ catch (error) {
950
+ console.error("Error creating relationship:", error);
951
+ return { content: [{ type: "text", text: `Failed to create relationship: ${error.message}` }], isError: true };
952
+ }
953
+ });
954
+ server.tool("delete-relationship", "Delete a relationship. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
955
+ metadataId: z.string().describe("Relationship MetadataId (GUID)")
956
+ }, async ({ metadataId }) => {
957
+ try {
958
+ checkCustomizationEnabled();
959
+ const service = getPowerPlatformService();
960
+ await service.deleteRelationship(metadataId);
961
+ return {
962
+ content: [{ type: "text", text: `✅ Successfully deleted relationship (${metadataId})\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.` }]
963
+ };
964
+ }
965
+ catch (error) {
966
+ console.error("Error deleting relationship:", error);
967
+ return { content: [{ type: "text", text: `Failed to delete relationship: ${error.message}` }], isError: true };
968
+ }
969
+ });
970
+ server.tool("update-relationship", "Update relationship labels. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
971
+ metadataId: z.string().describe("Relationship MetadataId (GUID)"),
972
+ referencedEntityNavigationPropertyName: z.string().optional().describe("Navigation property name"),
973
+ referencingEntityNavigationPropertyName: z.string().optional().describe("Navigation property name")
974
+ }, async (params) => {
975
+ try {
976
+ checkCustomizationEnabled();
977
+ const service = getPowerPlatformService();
978
+ const updates = {};
979
+ if (params.referencedEntityNavigationPropertyName)
980
+ updates.ReferencedEntityNavigationPropertyName = params.referencedEntityNavigationPropertyName;
981
+ if (params.referencingEntityNavigationPropertyName)
982
+ updates.ReferencingEntityNavigationPropertyName = params.referencingEntityNavigationPropertyName;
983
+ await service.updateRelationship(params.metadataId, updates);
984
+ return {
985
+ content: [{ type: "text", text: `✅ Successfully updated relationship (${params.metadataId})\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.` }]
986
+ };
987
+ }
988
+ catch (error) {
989
+ console.error("Error updating relationship:", error);
990
+ return { content: [{ type: "text", text: `Failed to update relationship: ${error.message}` }], isError: true };
991
+ }
992
+ });
993
+ server.tool("create-global-optionset-attribute", "Create a picklist attribute using an existing global option set. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
994
+ entityLogicalName: z.string().describe("Entity logical name"),
995
+ schemaName: z.string().describe("Attribute schema name"),
996
+ displayName: z.string().describe("Attribute display name"),
997
+ globalOptionSetName: z.string().describe("Global option set name to use"),
998
+ description: z.string().optional().describe("Attribute description"),
999
+ requiredLevel: z.enum(["None", "Recommended", "ApplicationRequired"]).optional().describe("Required level (default: None)"),
1000
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
1001
+ }, async (params) => {
1002
+ try {
1003
+ checkCustomizationEnabled();
1004
+ const service = getPowerPlatformService();
1005
+ // Look up the global option set to get its MetadataId
1006
+ const globalOptionSet = await service.getGlobalOptionSet(params.globalOptionSetName);
1007
+ const metadataId = globalOptionSet.MetadataId;
1008
+ const attributeDefinition = {
1009
+ "@odata.type": "Microsoft.Dynamics.CRM.PicklistAttributeMetadata",
1010
+ SchemaName: params.schemaName,
1011
+ DisplayName: {
1012
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
1013
+ LocalizedLabels: [{ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", Label: params.displayName, LanguageCode: 1033 }]
1014
+ },
1015
+ Description: {
1016
+ "@odata.type": "Microsoft.Dynamics.CRM.Label",
1017
+ LocalizedLabels: [{ "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", Label: params.description || "", LanguageCode: 1033 }]
1018
+ },
1019
+ RequiredLevel: { Value: params.requiredLevel || "None", CanBeChanged: true },
1020
+ "GlobalOptionSet@odata.bind": `/GlobalOptionSetDefinitions(${metadataId})`
1021
+ };
1022
+ const solution = params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1023
+ const result = await service.createGlobalOptionSetAttribute(params.entityLogicalName, attributeDefinition, solution);
1024
+ return {
1025
+ content: [{ type: "text", text: `✅ Successfully created global option set attribute '${params.schemaName}' using '${params.globalOptionSetName}'\n\n⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.` }]
1026
+ };
1027
+ }
1028
+ catch (error) {
1029
+ console.error("Error creating global option set attribute:", error);
1030
+ return { content: [{ type: "text", text: `Failed to create global option set attribute: ${error.message}` }], isError: true };
1031
+ }
1032
+ });
1033
+ server.tool("publish-customizations", "Publish all pending customizations in Dynamics 365. This makes all unpublished changes active. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {}, async () => {
1034
+ try {
1035
+ checkCustomizationEnabled();
1036
+ const service = getPowerPlatformService();
1037
+ await service.publishAllCustomizations();
1038
+ return {
1039
+ content: [
1040
+ {
1041
+ type: "text",
1042
+ text: "Successfully published all customizations. All pending changes are now active."
1043
+ }
1044
+ ]
1045
+ };
1046
+ }
1047
+ catch (error) {
1048
+ console.error("Error publishing customizations:", error);
1049
+ return {
1050
+ content: [
1051
+ {
1052
+ type: "text",
1053
+ text: `Failed to publish customizations: ${error.message}`
1054
+ }
1055
+ ],
1056
+ isError: true
1057
+ };
1058
+ }
1059
+ });
1060
+ server.tool("update-global-optionset", "Update a global option set in Dynamics 365. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1061
+ metadataId: z.string().describe("The MetadataId of the option set"),
1062
+ displayName: z.string().optional().describe("New display name"),
1063
+ description: z.string().optional().describe("New description"),
1064
+ solutionUniqueName: z.string().optional().describe("Solution to add to (optional, uses POWERPLATFORM_DEFAULT_SOLUTION if not provided)")
1065
+ }, async ({ metadataId, displayName, description, solutionUniqueName }) => {
1066
+ try {
1067
+ checkCustomizationEnabled();
1068
+ const service = getPowerPlatformService();
1069
+ const updates = { '@odata.type': 'Microsoft.Dynamics.CRM.OptionSetMetadata' };
1070
+ if (displayName) {
1071
+ updates.DisplayName = {
1072
+ LocalizedLabels: [{ Label: displayName, LanguageCode: 1033 }]
1073
+ };
1074
+ }
1075
+ if (description) {
1076
+ updates.Description = {
1077
+ LocalizedLabels: [{ Label: description, LanguageCode: 1033 }]
1078
+ };
1079
+ }
1080
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1081
+ await service.updateGlobalOptionSet(metadataId, updates, solution);
1082
+ return {
1083
+ content: [
1084
+ {
1085
+ type: "text",
1086
+ text: `✅ Successfully updated global option set (${metadataId})\n\n` +
1087
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1088
+ }
1089
+ ]
1090
+ };
1091
+ }
1092
+ catch (error) {
1093
+ console.error("Error updating global option set:", error);
1094
+ return {
1095
+ content: [{ type: "text", text: `Failed to update global option set: ${error.message}` }],
1096
+ isError: true
1097
+ };
1098
+ }
1099
+ });
1100
+ server.tool("add-optionset-value", "Add a new value to a global option set in Dynamics 365. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1101
+ optionSetName: z.string().describe("The name of the option set"),
1102
+ value: z.number().describe("The numeric value (should start with publisher prefix, e.g., 15743xxxx)"),
1103
+ label: z.string().describe("The display label for the value"),
1104
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
1105
+ }, async ({ optionSetName, value, label, solutionUniqueName }) => {
1106
+ try {
1107
+ checkCustomizationEnabled();
1108
+ const service = getPowerPlatformService();
1109
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1110
+ await service.addOptionSetValue(optionSetName, value, label, solution);
1111
+ return {
1112
+ content: [
1113
+ {
1114
+ type: "text",
1115
+ text: `✅ Successfully added value to option set '${optionSetName}'\n` +
1116
+ `Value: ${value}\n` +
1117
+ `Label: ${label}\n\n` +
1118
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1119
+ }
1120
+ ]
1121
+ };
1122
+ }
1123
+ catch (error) {
1124
+ console.error("Error adding option set value:", error);
1125
+ return {
1126
+ content: [{ type: "text", text: `Failed to add option set value: ${error.message}` }],
1127
+ isError: true
1128
+ };
1129
+ }
1130
+ });
1131
+ server.tool("update-optionset-value", "Update an existing value in a global option set. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1132
+ optionSetName: z.string().describe("The name of the option set"),
1133
+ value: z.number().describe("The numeric value to update"),
1134
+ label: z.string().describe("The new display label"),
1135
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
1136
+ }, async ({ optionSetName, value, label, solutionUniqueName }) => {
1137
+ try {
1138
+ checkCustomizationEnabled();
1139
+ const service = getPowerPlatformService();
1140
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1141
+ await service.updateOptionSetValue(optionSetName, value, label, solution);
1142
+ return {
1143
+ content: [
1144
+ {
1145
+ type: "text",
1146
+ text: `✅ Successfully updated value in option set '${optionSetName}'\n` +
1147
+ `Value: ${value}\n` +
1148
+ `New Label: ${label}\n\n` +
1149
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1150
+ }
1151
+ ]
1152
+ };
1153
+ }
1154
+ catch (error) {
1155
+ console.error("Error updating option set value:", error);
1156
+ return {
1157
+ content: [{ type: "text", text: `Failed to update option set value: ${error.message}` }],
1158
+ isError: true
1159
+ };
1160
+ }
1161
+ });
1162
+ server.tool("delete-optionset-value", "Delete a value from a global option set. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1163
+ optionSetName: z.string().describe("The name of the option set"),
1164
+ value: z.number().describe("The numeric value to delete")
1165
+ }, async ({ optionSetName, value }) => {
1166
+ try {
1167
+ checkCustomizationEnabled();
1168
+ const service = getPowerPlatformService();
1169
+ await service.deleteOptionSetValue(optionSetName, value);
1170
+ return {
1171
+ content: [
1172
+ {
1173
+ type: "text",
1174
+ text: `✅ Successfully deleted value ${value} from option set '${optionSetName}'\n\n` +
1175
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1176
+ }
1177
+ ]
1178
+ };
1179
+ }
1180
+ catch (error) {
1181
+ console.error("Error deleting option set value:", error);
1182
+ return {
1183
+ content: [{ type: "text", text: `Failed to delete option set value: ${error.message}` }],
1184
+ isError: true
1185
+ };
1186
+ }
1187
+ });
1188
+ server.tool("reorder-optionset-values", "Reorder the values in a global option set. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1189
+ optionSetName: z.string().describe("The name of the option set"),
1190
+ values: z.array(z.number()).describe("Array of values in the desired order"),
1191
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
1192
+ }, async ({ optionSetName, values, solutionUniqueName }) => {
1193
+ try {
1194
+ checkCustomizationEnabled();
1195
+ const service = getPowerPlatformService();
1196
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1197
+ await service.reorderOptionSetValues(optionSetName, values, solution);
1198
+ return {
1199
+ content: [
1200
+ {
1201
+ type: "text",
1202
+ text: `✅ Successfully reordered ${values.length} values in option set '${optionSetName}'\n\n` +
1203
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1204
+ }
1205
+ ]
1206
+ };
1207
+ }
1208
+ catch (error) {
1209
+ console.error("Error reordering option set values:", error);
1210
+ return {
1211
+ content: [{ type: "text", text: `Failed to reorder option set values: ${error.message}` }],
1212
+ isError: true
1213
+ };
1214
+ }
1215
+ });
1216
+ server.tool("create-form", "Create a new form (Main, QuickCreate, QuickView, Card) for an entity. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1217
+ name: z.string().describe("Form name"),
1218
+ entityLogicalName: z.string().describe("Entity logical name"),
1219
+ formType: z.enum(["Main", "QuickCreate", "QuickView", "Card"]).describe("Form type"),
1220
+ formXml: z.string().describe("Form XML definition"),
1221
+ description: z.string().optional().describe("Form description"),
1222
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
1223
+ }, async ({ name, entityLogicalName, formType, formXml, description, solutionUniqueName }) => {
1224
+ try {
1225
+ checkCustomizationEnabled();
1226
+ const service = getPowerPlatformService();
1227
+ const typeMap = { Main: 2, QuickCreate: 7, QuickView: 8, Card: 10 };
1228
+ const form = {
1229
+ name,
1230
+ objecttypecode: entityLogicalName,
1231
+ type: typeMap[formType],
1232
+ formxml: formXml,
1233
+ description: description || ""
1234
+ };
1235
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1236
+ const result = await service.createForm(form, solution);
1237
+ return {
1238
+ content: [
1239
+ {
1240
+ type: "text",
1241
+ text: `✅ Successfully created ${formType} form '${name}' for entity '${entityLogicalName}'\n` +
1242
+ `Form ID: ${result.formid}\n\n` +
1243
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1244
+ }
1245
+ ]
1246
+ };
1247
+ }
1248
+ catch (error) {
1249
+ console.error("Error creating form:", error);
1250
+ return {
1251
+ content: [{ type: "text", text: `Failed to create form: ${error.message}` }],
1252
+ isError: true
1253
+ };
1254
+ }
1255
+ });
1256
+ server.tool("update-form", "Update an existing form. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1257
+ formId: z.string().describe("Form ID (GUID)"),
1258
+ name: z.string().optional().describe("New form name"),
1259
+ formXml: z.string().optional().describe("New form XML definition"),
1260
+ description: z.string().optional().describe("New description"),
1261
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
1262
+ }, async ({ formId, name, formXml, description, solutionUniqueName }) => {
1263
+ try {
1264
+ checkCustomizationEnabled();
1265
+ const service = getPowerPlatformService();
1266
+ const updates = {};
1267
+ if (name)
1268
+ updates.name = name;
1269
+ if (formXml)
1270
+ updates.formxml = formXml;
1271
+ if (description)
1272
+ updates.description = description;
1273
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1274
+ await service.updateForm(formId, updates, solution);
1275
+ return {
1276
+ content: [
1277
+ {
1278
+ type: "text",
1279
+ text: `✅ Successfully updated form (${formId})\n\n` +
1280
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1281
+ }
1282
+ ]
1283
+ };
1284
+ }
1285
+ catch (error) {
1286
+ console.error("Error updating form:", error);
1287
+ return {
1288
+ content: [{ type: "text", text: `Failed to update form: ${error.message}` }],
1289
+ isError: true
1290
+ };
1291
+ }
1292
+ });
1293
+ server.tool("delete-form", "Delete a form. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1294
+ formId: z.string().describe("Form ID (GUID)")
1295
+ }, async ({ formId }) => {
1296
+ try {
1297
+ checkCustomizationEnabled();
1298
+ const service = getPowerPlatformService();
1299
+ await service.deleteForm(formId);
1300
+ return {
1301
+ content: [
1302
+ {
1303
+ type: "text",
1304
+ text: `✅ Successfully deleted form (${formId})\n\n` +
1305
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1306
+ }
1307
+ ]
1308
+ };
1309
+ }
1310
+ catch (error) {
1311
+ console.error("Error deleting form:", error);
1312
+ return {
1313
+ content: [{ type: "text", text: `Failed to delete form: ${error.message}` }],
1314
+ isError: true
1315
+ };
1316
+ }
1317
+ });
1318
+ server.tool("activate-form", "Activate a form. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1319
+ formId: z.string().describe("Form ID (GUID)")
1320
+ }, async ({ formId }) => {
1321
+ try {
1322
+ checkCustomizationEnabled();
1323
+ const service = getPowerPlatformService();
1324
+ await service.activateForm(formId);
1325
+ return {
1326
+ content: [
1327
+ {
1328
+ type: "text",
1329
+ text: `✅ Successfully activated form (${formId})\n\n` +
1330
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1331
+ }
1332
+ ]
1333
+ };
1334
+ }
1335
+ catch (error) {
1336
+ console.error("Error activating form:", error);
1337
+ return {
1338
+ content: [{ type: "text", text: `Failed to activate form: ${error.message}` }],
1339
+ isError: true
1340
+ };
1341
+ }
1342
+ });
1343
+ server.tool("deactivate-form", "Deactivate a form. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1344
+ formId: z.string().describe("Form ID (GUID)")
1345
+ }, async ({ formId }) => {
1346
+ try {
1347
+ checkCustomizationEnabled();
1348
+ const service = getPowerPlatformService();
1349
+ await service.deactivateForm(formId);
1350
+ return {
1351
+ content: [
1352
+ {
1353
+ type: "text",
1354
+ text: `✅ Successfully deactivated form (${formId})\n\n` +
1355
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1356
+ }
1357
+ ]
1358
+ };
1359
+ }
1360
+ catch (error) {
1361
+ console.error("Error deactivating form:", error);
1362
+ return {
1363
+ content: [{ type: "text", text: `Failed to deactivate form: ${error.message}` }],
1364
+ isError: true
1365
+ };
1366
+ }
1367
+ });
1368
+ server.tool("create-view", "Create a new view for an entity using FetchXML. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1369
+ name: z.string().describe("View name"),
1370
+ entityLogicalName: z.string().describe("Entity logical name"),
1371
+ fetchXml: z.string().describe("FetchXML query"),
1372
+ layoutXml: z.string().describe("Layout XML (column definitions)"),
1373
+ queryType: z.number().optional().describe("Query type (default: 0 for public view)"),
1374
+ isDefault: z.boolean().optional().describe("Set as default view"),
1375
+ description: z.string().optional().describe("View description"),
1376
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
1377
+ }, async ({ name, entityLogicalName, fetchXml, layoutXml, queryType, isDefault, description, solutionUniqueName }) => {
1378
+ try {
1379
+ checkCustomizationEnabled();
1380
+ const service = getPowerPlatformService();
1381
+ const view = {
1382
+ name,
1383
+ returnedtypecode: entityLogicalName,
1384
+ fetchxml: fetchXml,
1385
+ layoutxml: layoutXml,
1386
+ querytype: queryType || 0,
1387
+ isdefault: isDefault || false,
1388
+ description: description || ""
1389
+ };
1390
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1391
+ const result = await service.createView(view, solution);
1392
+ return {
1393
+ content: [
1394
+ {
1395
+ type: "text",
1396
+ text: `✅ Successfully created view '${name}' for entity '${entityLogicalName}'\n` +
1397
+ `View ID: ${result.savedqueryid}\n\n` +
1398
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1399
+ }
1400
+ ]
1401
+ };
1402
+ }
1403
+ catch (error) {
1404
+ console.error("Error creating view:", error);
1405
+ return {
1406
+ content: [{ type: "text", text: `Failed to create view: ${error.message}` }],
1407
+ isError: true
1408
+ };
1409
+ }
1410
+ });
1411
+ server.tool("update-view", "Update an existing view. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1412
+ viewId: z.string().describe("View ID (GUID)"),
1413
+ name: z.string().optional().describe("New view name"),
1414
+ fetchXml: z.string().optional().describe("New FetchXML query"),
1415
+ layoutXml: z.string().optional().describe("New layout XML"),
1416
+ isDefault: z.boolean().optional().describe("Set as default view"),
1417
+ description: z.string().optional().describe("New description"),
1418
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
1419
+ }, async ({ viewId, name, fetchXml, layoutXml, isDefault, description, solutionUniqueName }) => {
1420
+ try {
1421
+ checkCustomizationEnabled();
1422
+ const service = getPowerPlatformService();
1423
+ const updates = {};
1424
+ if (name)
1425
+ updates.name = name;
1426
+ if (fetchXml)
1427
+ updates.fetchxml = fetchXml;
1428
+ if (layoutXml)
1429
+ updates.layoutxml = layoutXml;
1430
+ if (isDefault !== undefined)
1431
+ updates.isdefault = isDefault;
1432
+ if (description)
1433
+ updates.description = description;
1434
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1435
+ await service.updateView(viewId, updates, solution);
1436
+ return {
1437
+ content: [
1438
+ {
1439
+ type: "text",
1440
+ text: `✅ Successfully updated view (${viewId})\n\n` +
1441
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1442
+ }
1443
+ ]
1444
+ };
1445
+ }
1446
+ catch (error) {
1447
+ console.error("Error updating view:", error);
1448
+ return {
1449
+ content: [{ type: "text", text: `Failed to update view: ${error.message}` }],
1450
+ isError: true
1451
+ };
1452
+ }
1453
+ });
1454
+ server.tool("delete-view", "Delete a view. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1455
+ viewId: z.string().describe("View ID (GUID)")
1456
+ }, async ({ viewId }) => {
1457
+ try {
1458
+ checkCustomizationEnabled();
1459
+ const service = getPowerPlatformService();
1460
+ await service.deleteView(viewId);
1461
+ return {
1462
+ content: [
1463
+ {
1464
+ type: "text",
1465
+ text: `✅ Successfully deleted view (${viewId})\n\n` +
1466
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1467
+ }
1468
+ ]
1469
+ };
1470
+ }
1471
+ catch (error) {
1472
+ console.error("Error deleting view:", error);
1473
+ return {
1474
+ content: [{ type: "text", text: `Failed to delete view: ${error.message}` }],
1475
+ isError: true
1476
+ };
1477
+ }
1478
+ });
1479
+ server.tool("set-default-view", "Set a view as the default view for its entity. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1480
+ viewId: z.string().describe("View ID (GUID)")
1481
+ }, async ({ viewId }) => {
1482
+ try {
1483
+ checkCustomizationEnabled();
1484
+ const service = getPowerPlatformService();
1485
+ await service.setDefaultView(viewId);
1486
+ return {
1487
+ content: [
1488
+ {
1489
+ type: "text",
1490
+ text: `✅ Successfully set view (${viewId}) as default\n\n` +
1491
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1492
+ }
1493
+ ]
1494
+ };
1495
+ }
1496
+ catch (error) {
1497
+ console.error("Error setting default view:", error);
1498
+ return {
1499
+ content: [{ type: "text", text: `Failed to set default view: ${error.message}` }],
1500
+ isError: true
1501
+ };
1502
+ }
1503
+ });
1504
+ server.tool("create-web-resource", "Create a new web resource (JavaScript, CSS, HTML, Image, etc.). Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1505
+ name: z.string().describe("Web resource name (must include prefix, e.g., 'prefix_/scripts/file.js')"),
1506
+ displayName: z.string().describe("Display name"),
1507
+ webResourceType: z.number().describe("Web resource type: 1=HTML, 2=CSS, 3=JavaScript, 4=XML, 5=PNG, 6=JPG, 7=GIF, 8=XAP, 9=XSL, 10=ICO"),
1508
+ content: z.string().describe("Base64-encoded content"),
1509
+ description: z.string().optional().describe("Description"),
1510
+ solutionUniqueName: z.string().optional().describe("Solution to add to")
1511
+ }, async ({ name, displayName, webResourceType, content, description, solutionUniqueName }) => {
1512
+ try {
1513
+ checkCustomizationEnabled();
1514
+ const service = getPowerPlatformService();
1515
+ const webResource = {
1516
+ name,
1517
+ displayname: displayName,
1518
+ webresourcetype: webResourceType,
1519
+ content,
1520
+ description: description || ""
1521
+ };
1522
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1523
+ const result = await service.createWebResource(webResource, solution);
1524
+ return {
1525
+ content: [
1526
+ {
1527
+ type: "text",
1528
+ text: `✅ Successfully created web resource '${name}'\n` +
1529
+ `Web Resource ID: ${result.webresourceid}\n\n` +
1530
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1531
+ }
1532
+ ]
1533
+ };
1534
+ }
1535
+ catch (error) {
1536
+ console.error("Error creating web resource:", error);
1537
+ return {
1538
+ content: [{ type: "text", text: `Failed to create web resource: ${error.message}` }],
1539
+ isError: true
1540
+ };
1541
+ }
1542
+ });
1543
+ server.tool("update-web-resource", "Update an existing web resource. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1544
+ webResourceId: z.string().describe("Web resource ID (GUID)"),
1545
+ displayName: z.string().optional().describe("Display name"),
1546
+ content: z.string().optional().describe("Base64-encoded content"),
1547
+ description: z.string().optional().describe("Description"),
1548
+ solutionUniqueName: z.string().optional().describe("Solution context")
1549
+ }, async ({ webResourceId, displayName, content, description, solutionUniqueName }) => {
1550
+ try {
1551
+ checkCustomizationEnabled();
1552
+ const service = getPowerPlatformService();
1553
+ const updates = {};
1554
+ if (displayName)
1555
+ updates.displayname = displayName;
1556
+ if (content)
1557
+ updates.content = content;
1558
+ if (description)
1559
+ updates.description = description;
1560
+ const solution = solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION;
1561
+ await service.updateWebResource(webResourceId, updates, solution);
1562
+ return {
1563
+ content: [
1564
+ {
1565
+ type: "text",
1566
+ text: `✅ Successfully updated web resource '${webResourceId}'\n\n` +
1567
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1568
+ }
1569
+ ]
1570
+ };
1571
+ }
1572
+ catch (error) {
1573
+ console.error("Error updating web resource:", error);
1574
+ return {
1575
+ content: [{ type: "text", text: `Failed to update web resource: ${error.message}` }],
1576
+ isError: true
1577
+ };
1578
+ }
1579
+ });
1580
+ server.tool("delete-web-resource", "Delete a web resource. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1581
+ webResourceId: z.string().describe("Web resource ID (GUID)")
1582
+ }, async ({ webResourceId }) => {
1583
+ try {
1584
+ checkCustomizationEnabled();
1585
+ const service = getPowerPlatformService();
1586
+ await service.deleteWebResource(webResourceId);
1587
+ return {
1588
+ content: [
1589
+ {
1590
+ type: "text",
1591
+ text: `✅ Successfully deleted web resource '${webResourceId}'\n\n` +
1592
+ `⚠️ IMPORTANT: You must publish this customization using the 'publish-customizations' tool before it becomes active.`
1593
+ }
1594
+ ]
1595
+ };
1596
+ }
1597
+ catch (error) {
1598
+ console.error("Error deleting web resource:", error);
1599
+ return {
1600
+ content: [{ type: "text", text: `Failed to delete web resource: ${error.message}` }],
1601
+ isError: true
1602
+ };
1603
+ }
1604
+ });
1605
+ server.tool("create-publisher", "Create a new solution publisher. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1606
+ uniqueName: z.string().describe("Publisher unique name"),
1607
+ friendlyName: z.string().describe("Publisher display name"),
1608
+ customizationPrefix: z.string().describe("Customization prefix (e.g., 'new')"),
1609
+ customizationOptionValuePrefix: z.number().describe("Option value prefix (e.g., 10000)"),
1610
+ description: z.string().optional().describe("Publisher description")
1611
+ }, async ({ uniqueName, friendlyName, customizationPrefix, customizationOptionValuePrefix, description }) => {
1612
+ try {
1613
+ checkCustomizationEnabled();
1614
+ const service = getPowerPlatformService();
1615
+ const publisher = {
1616
+ uniquename: uniqueName,
1617
+ friendlyname: friendlyName,
1618
+ customizationprefix: customizationPrefix,
1619
+ customizationoptionvalueprefix: customizationOptionValuePrefix,
1620
+ description: description || ""
1621
+ };
1622
+ const result = await service.createPublisher(publisher);
1623
+ return {
1624
+ content: [
1625
+ {
1626
+ type: "text",
1627
+ text: `✅ Successfully created publisher '${friendlyName}'\n` +
1628
+ `Unique Name: ${uniqueName}\n` +
1629
+ `Prefix: ${customizationPrefix}\n` +
1630
+ `Option Value Prefix: ${customizationOptionValuePrefix}\n` +
1631
+ `Publisher ID: ${result.publisherid}`
1632
+ }
1633
+ ]
1634
+ };
1635
+ }
1636
+ catch (error) {
1637
+ console.error("Error creating publisher:", error);
1638
+ return {
1639
+ content: [{ type: "text", text: `Failed to create publisher: ${error.message}` }],
1640
+ isError: true
1641
+ };
1642
+ }
1643
+ });
1644
+ server.tool("create-solution", "Create a new solution. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1645
+ uniqueName: z.string().describe("Solution unique name"),
1646
+ friendlyName: z.string().describe("Solution display name"),
1647
+ version: z.string().describe("Solution version (e.g., '1.0.0.0')"),
1648
+ publisherId: z.string().describe("Publisher ID (GUID)"),
1649
+ description: z.string().optional().describe("Solution description")
1650
+ }, async ({ uniqueName, friendlyName, version, publisherId, description }) => {
1651
+ try {
1652
+ checkCustomizationEnabled();
1653
+ const service = getPowerPlatformService();
1654
+ const solution = {
1655
+ uniquename: uniqueName,
1656
+ friendlyname: friendlyName,
1657
+ version,
1658
+ "publisherid@odata.bind": `/publishers(${publisherId})`,
1659
+ description: description || ""
1660
+ };
1661
+ const result = await service.createSolution(solution);
1662
+ return {
1663
+ content: [
1664
+ {
1665
+ type: "text",
1666
+ text: `✅ Successfully created solution '${friendlyName}'\n` +
1667
+ `Unique Name: ${uniqueName}\n` +
1668
+ `Version: ${version}\n` +
1669
+ `Solution ID: ${result.solutionid}`
1670
+ }
1671
+ ]
1672
+ };
1673
+ }
1674
+ catch (error) {
1675
+ console.error("Error creating solution:", error);
1676
+ return {
1677
+ content: [{ type: "text", text: `Failed to create solution: ${error.message}` }],
1678
+ isError: true
1679
+ };
1680
+ }
1681
+ });
1682
+ server.tool("add-solution-component", "Add a component to a solution. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1683
+ solutionUniqueName: z.string().describe("Solution unique name"),
1684
+ componentId: z.string().describe("Component ID (GUID or MetadataId)"),
1685
+ componentType: z.number().describe("Component type: 1=Entity, 2=Attribute, 9=OptionSet, 24=Form, 26=SavedQuery, 29=Workflow, 60=SystemForm, 61=WebResource"),
1686
+ addRequiredComponents: z.boolean().optional().describe("Add required components (default: true)"),
1687
+ includedComponentSettingsValues: z.string().optional().describe("Component settings values")
1688
+ }, async ({ solutionUniqueName, componentId, componentType, addRequiredComponents, includedComponentSettingsValues }) => {
1689
+ try {
1690
+ checkCustomizationEnabled();
1691
+ const service = getPowerPlatformService();
1692
+ await service.addComponentToSolution(solutionUniqueName, componentId, componentType, addRequiredComponents ?? true, includedComponentSettingsValues);
1693
+ return {
1694
+ content: [
1695
+ {
1696
+ type: "text",
1697
+ text: `✅ Successfully added component '${componentId}' (type: ${componentType}) to solution '${solutionUniqueName}'`
1698
+ }
1699
+ ]
1700
+ };
1701
+ }
1702
+ catch (error) {
1703
+ console.error("Error adding component to solution:", error);
1704
+ return {
1705
+ content: [{ type: "text", text: `Failed to add component to solution: ${error.message}` }],
1706
+ isError: true
1707
+ };
1708
+ }
1709
+ });
1710
+ server.tool("remove-solution-component", "Remove a component from a solution. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1711
+ solutionUniqueName: z.string().describe("Solution unique name"),
1712
+ componentId: z.string().describe("Component ID (GUID or MetadataId)"),
1713
+ componentType: z.number().describe("Component type: 1=Entity, 2=Attribute, 9=OptionSet, 24=Form, 26=SavedQuery, 29=Workflow, 60=SystemForm, 61=WebResource")
1714
+ }, async ({ solutionUniqueName, componentId, componentType }) => {
1715
+ try {
1716
+ checkCustomizationEnabled();
1717
+ const service = getPowerPlatformService();
1718
+ await service.removeComponentFromSolution(solutionUniqueName, componentId, componentType);
1719
+ return {
1720
+ content: [
1721
+ {
1722
+ type: "text",
1723
+ text: `✅ Successfully removed component '${componentId}' (type: ${componentType}) from solution '${solutionUniqueName}'`
1724
+ }
1725
+ ]
1726
+ };
1727
+ }
1728
+ catch (error) {
1729
+ console.error("Error removing component from solution:", error);
1730
+ return {
1731
+ content: [{ type: "text", text: `Failed to remove component from solution: ${error.message}` }],
1732
+ isError: true
1733
+ };
1734
+ }
1735
+ });
1736
+ server.tool("export-solution", "Export a solution as a zip file. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1737
+ solutionName: z.string().describe("Solution unique name"),
1738
+ managed: z.boolean().optional().describe("Export as managed solution (default: false)")
1739
+ }, async ({ solutionName, managed }) => {
1740
+ try {
1741
+ checkCustomizationEnabled();
1742
+ const service = getPowerPlatformService();
1743
+ const result = await service.exportSolution(solutionName, managed ?? false);
1744
+ return {
1745
+ content: [
1746
+ {
1747
+ type: "text",
1748
+ text: `✅ Successfully exported solution '${solutionName}' as ${managed ? 'managed' : 'unmanaged'}\n\n` +
1749
+ `Export File (Base64): ${result.ExportSolutionFile.substring(0, 100)}...`
1750
+ }
1751
+ ]
1752
+ };
1753
+ }
1754
+ catch (error) {
1755
+ console.error("Error exporting solution:", error);
1756
+ return {
1757
+ content: [{ type: "text", text: `Failed to export solution: ${error.message}` }],
1758
+ isError: true
1759
+ };
1760
+ }
1761
+ });
1762
+ server.tool("import-solution", "Import a solution from a base64-encoded zip file. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1763
+ customizationFile: z.string().describe("Base64-encoded solution zip file"),
1764
+ publishWorkflows: z.boolean().optional().describe("Publish workflows after import (default: true)"),
1765
+ overwriteUnmanagedCustomizations: z.boolean().optional().describe("Overwrite unmanaged customizations (default: false)")
1766
+ }, async ({ customizationFile, publishWorkflows, overwriteUnmanagedCustomizations }) => {
1767
+ try {
1768
+ checkCustomizationEnabled();
1769
+ const service = getPowerPlatformService();
1770
+ const result = await service.importSolution(customizationFile, publishWorkflows ?? true, overwriteUnmanagedCustomizations ?? false);
1771
+ return {
1772
+ content: [
1773
+ {
1774
+ type: "text",
1775
+ text: `✅ Successfully initiated solution import\n` +
1776
+ `Import Job ID: ${result.ImportJobId}\n\n` +
1777
+ `⚠️ NOTE: Solution import is asynchronous. Monitor the import job for completion status.`
1778
+ }
1779
+ ]
1780
+ };
1781
+ }
1782
+ catch (error) {
1783
+ console.error("Error importing solution:", error);
1784
+ return {
1785
+ content: [{ type: "text", text: `Failed to import solution: ${error.message}` }],
1786
+ isError: true
1787
+ };
1788
+ }
1789
+ });
1790
+ server.tool("publish-entity", "Publish all customizations for a specific entity. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1791
+ entityLogicalName: z.string().describe("Entity logical name to publish")
1792
+ }, async ({ entityLogicalName }) => {
1793
+ try {
1794
+ checkCustomizationEnabled();
1795
+ const service = getPowerPlatformService();
1796
+ await service.publishEntity(entityLogicalName);
1797
+ return {
1798
+ content: [
1799
+ {
1800
+ type: "text",
1801
+ text: `✅ Successfully published entity '${entityLogicalName}'\n\n` +
1802
+ `All customizations for this entity are now active in the environment.`
1803
+ }
1804
+ ]
1805
+ };
1806
+ }
1807
+ catch (error) {
1808
+ console.error("Error publishing entity:", error);
1809
+ return {
1810
+ content: [{ type: "text", text: `Failed to publish entity: ${error.message}` }],
1811
+ isError: true
1812
+ };
1813
+ }
1814
+ });
1815
+ // ============================================================
1816
+ // PLUGIN DEPLOYMENT TOOLS
1817
+ // ============================================================
1818
+ server.tool("create-plugin-assembly", "Upload a compiled plugin DLL to Dynamics 365 from local file system. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1819
+ assemblyPath: z.string().describe("Local file path to compiled DLL (e.g., C:\\Dev\\MyPlugin\\bin\\Release\\net462\\MyPlugin.dll)"),
1820
+ assemblyName: z.string().describe("Friendly name for the assembly (e.g., MyPlugin)"),
1821
+ version: z.string().optional().describe("Version string (auto-extracted if omitted, e.g., '1.0.0.0')"),
1822
+ isolationMode: z.number().optional().describe("Isolation mode: 2=Sandbox (default, required for production)"),
1823
+ description: z.string().optional().describe("Assembly description"),
1824
+ solutionUniqueName: z.string().optional().describe("Solution to add assembly to"),
1825
+ }, async ({ assemblyPath, assemblyName, version, isolationMode, description, solutionUniqueName }) => {
1826
+ try {
1827
+ checkCustomizationEnabled();
1828
+ const service = getPowerPlatformService();
1829
+ // Read DLL file from file system (Windows or WSL compatible)
1830
+ const fs = await import('fs/promises');
1831
+ const normalizedPath = assemblyPath.replace(/\\/g, '/'); // Normalize path
1832
+ const dllBuffer = await fs.readFile(normalizedPath);
1833
+ const dllBase64 = dllBuffer.toString('base64');
1834
+ // Validate DLL format (check for "MZ" header - .NET assembly signature)
1835
+ const header = dllBuffer.toString('utf8', 0, 2);
1836
+ if (header !== 'MZ') {
1837
+ throw new Error('Invalid .NET assembly format (missing MZ header)');
1838
+ }
1839
+ // Extract version if not provided
1840
+ const extractedVersion = version || await service.extractAssemblyVersion(assemblyPath);
1841
+ // Upload assembly
1842
+ const result = await service.createPluginAssembly({
1843
+ name: assemblyName,
1844
+ content: dllBase64,
1845
+ version: extractedVersion,
1846
+ isolationMode: isolationMode ?? 2, // Default to Sandbox for security
1847
+ description,
1848
+ solutionUniqueName: solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION,
1849
+ });
1850
+ return {
1851
+ content: [{
1852
+ type: "text",
1853
+ text: `✅ Plugin assembly '${assemblyName}' uploaded successfully\n\n` +
1854
+ `📦 Assembly ID: ${result.pluginAssemblyId}\n` +
1855
+ `🔢 Version: ${extractedVersion}\n` +
1856
+ `💾 Size: ${(dllBuffer.length / 1024).toFixed(2)} KB\n` +
1857
+ `🔌 Plugin Types Created: ${result.pluginTypes.length}\n\n` +
1858
+ `Plugin Types:\n${result.pluginTypes.map(t => ` • ${t.typeName} (${t.pluginTypeId})`).join('\n') || ' (none created yet - check System Jobs)'}`
1859
+ }]
1860
+ };
1861
+ }
1862
+ catch (error) {
1863
+ console.error("Error creating plugin assembly:", error);
1864
+ return {
1865
+ content: [{ type: "text", text: `❌ Failed to create plugin assembly: ${error.message}` }],
1866
+ isError: true
1867
+ };
1868
+ }
1869
+ });
1870
+ server.tool("update-plugin-assembly", "Update an existing plugin assembly with new compiled DLL. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1871
+ assemblyId: z.string().describe("Assembly ID (GUID)"),
1872
+ assemblyPath: z.string().describe("Local file path to new compiled DLL"),
1873
+ version: z.string().optional().describe("Version string (auto-extracted if omitted)"),
1874
+ solutionUniqueName: z.string().optional().describe("Solution context"),
1875
+ }, async ({ assemblyId, assemblyPath, version, solutionUniqueName }) => {
1876
+ try {
1877
+ checkCustomizationEnabled();
1878
+ const service = getPowerPlatformService();
1879
+ // Read new DLL
1880
+ const fs = await import('fs/promises');
1881
+ const normalizedPath = assemblyPath.replace(/\\/g, '/');
1882
+ const dllBuffer = await fs.readFile(normalizedPath);
1883
+ const dllBase64 = dllBuffer.toString('base64');
1884
+ // Extract version if not provided
1885
+ const extractedVersion = version || await service.extractAssemblyVersion(assemblyPath);
1886
+ // Update assembly
1887
+ await service.updatePluginAssembly(assemblyId, dllBase64, extractedVersion, solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION);
1888
+ return {
1889
+ content: [{
1890
+ type: "text",
1891
+ text: `✅ Plugin assembly updated successfully\n\n` +
1892
+ `📦 Assembly ID: ${assemblyId}\n` +
1893
+ `🔢 Version: ${extractedVersion}\n` +
1894
+ `💾 Size: ${(dllBuffer.length / 1024).toFixed(2)} KB\n\n` +
1895
+ `⚠️ Note: Existing plugin steps remain registered and active.`
1896
+ }]
1897
+ };
1898
+ }
1899
+ catch (error) {
1900
+ console.error("Error updating plugin assembly:", error);
1901
+ return {
1902
+ content: [{ type: "text", text: `❌ Failed to update plugin assembly: ${error.message}` }],
1903
+ isError: true
1904
+ };
1905
+ }
1906
+ });
1907
+ server.tool("register-plugin-step", "Register a plugin step on an SDK message. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1908
+ assemblyName: z.string().describe("Assembly name (e.g., MyPlugin)"),
1909
+ pluginTypeName: z.string().describe("Full type name (e.g., MyOrg.Plugins.ContactPlugin)"),
1910
+ stepName: z.string().describe("Friendly step name (e.g., 'Contact: Update - Post-Operation')"),
1911
+ messageName: z.string().describe("SDK message: Create, Update, Delete, SetState, etc."),
1912
+ primaryEntity: z.string().describe("Entity logical name (e.g., contact, account)"),
1913
+ stage: z.enum(['PreValidation', 'PreOperation', 'PostOperation']).describe("Execution stage"),
1914
+ executionMode: z.enum(['Sync', 'Async']).describe("Execution mode"),
1915
+ rank: z.number().optional().describe("Execution order (default: 1, lower runs first)"),
1916
+ filteringAttributes: z.array(z.string()).optional().describe("Fields to monitor (e.g., ['firstname', 'lastname'])"),
1917
+ configuration: z.string().optional().describe("Secure/unsecure config JSON"),
1918
+ solutionUniqueName: z.string().optional(),
1919
+ }, async (params) => {
1920
+ try {
1921
+ checkCustomizationEnabled();
1922
+ const service = getPowerPlatformService();
1923
+ // Resolve plugin type ID by typename
1924
+ const pluginTypeId = await service.queryPluginTypeByTypename(params.pluginTypeName);
1925
+ // Map stage and mode enums to numbers
1926
+ const stageMap = {
1927
+ PreValidation: 10,
1928
+ PreOperation: 20,
1929
+ PostOperation: 40
1930
+ };
1931
+ const modeMap = { Sync: 0, Async: 1 };
1932
+ // Register step
1933
+ const result = await service.registerPluginStep({
1934
+ pluginTypeId,
1935
+ name: params.stepName,
1936
+ messageName: params.messageName,
1937
+ primaryEntityName: params.primaryEntity,
1938
+ stage: stageMap[params.stage],
1939
+ executionMode: modeMap[params.executionMode],
1940
+ rank: params.rank ?? 1,
1941
+ filteringAttributes: params.filteringAttributes?.join(','),
1942
+ configuration: params.configuration,
1943
+ solutionUniqueName: params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION,
1944
+ });
1945
+ return {
1946
+ content: [{
1947
+ type: "text",
1948
+ text: `✅ Plugin step '${params.stepName}' registered successfully\n\n` +
1949
+ `🆔 Step ID: ${result.stepId}\n` +
1950
+ `📨 Message: ${params.messageName}\n` +
1951
+ `📊 Entity: ${params.primaryEntity}\n` +
1952
+ `⏱️ Stage: ${params.stage}\n` +
1953
+ `🔄 Mode: ${params.executionMode}\n` +
1954
+ `📋 Rank: ${params.rank ?? 1}\n` +
1955
+ (params.filteringAttributes?.length ? `🔍 Filtering: ${params.filteringAttributes.join(', ')}\n` : '')
1956
+ }]
1957
+ };
1958
+ }
1959
+ catch (error) {
1960
+ console.error("Error registering plugin step:", error);
1961
+ return {
1962
+ content: [{ type: "text", text: `❌ Failed to register plugin step: ${error.message}` }],
1963
+ isError: true
1964
+ };
1965
+ }
1966
+ });
1967
+ server.tool("register-plugin-image", "Add a pre/post image to a plugin step for accessing entity data. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
1968
+ stepId: z.string().describe("Plugin step ID (from register-plugin-step)"),
1969
+ imageName: z.string().describe("Image name (e.g., 'PreImage', 'PostImage')"),
1970
+ imageType: z.enum(['PreImage', 'PostImage', 'Both']).describe("Image type"),
1971
+ entityAlias: z.string().describe("Alias for code access (e.g., 'target', 'preimage')"),
1972
+ attributes: z.array(z.string()).optional().describe("Attributes to include (empty = all)"),
1973
+ messagePropertyName: z.string().optional().describe("Message property (default: 'Target')"),
1974
+ }, async (params) => {
1975
+ try {
1976
+ checkCustomizationEnabled();
1977
+ const service = getPowerPlatformService();
1978
+ const imageTypeMap = { PreImage: 0, PostImage: 1, Both: 2 };
1979
+ const result = await service.registerPluginImage({
1980
+ stepId: params.stepId,
1981
+ name: params.imageName,
1982
+ imageType: imageTypeMap[params.imageType],
1983
+ entityAlias: params.entityAlias,
1984
+ attributes: params.attributes?.join(','),
1985
+ messagePropertyName: params.messagePropertyName || 'Target',
1986
+ });
1987
+ return {
1988
+ content: [{
1989
+ type: "text",
1990
+ text: `✅ Plugin image '${params.imageName}' registered successfully\n\n` +
1991
+ `🆔 Image ID: ${result.imageId}\n` +
1992
+ `🖼️ Type: ${params.imageType}\n` +
1993
+ `🏷️ Alias: ${params.entityAlias}\n` +
1994
+ `📋 Attributes: ${params.attributes?.length ? params.attributes.join(', ') : 'All attributes'}`
1995
+ }]
1996
+ };
1997
+ }
1998
+ catch (error) {
1999
+ console.error("Error registering plugin image:", error);
2000
+ return {
2001
+ content: [{ type: "text", text: `❌ Failed to register plugin image: ${error.message}` }],
2002
+ isError: true
2003
+ };
2004
+ }
2005
+ });
2006
+ server.tool("deploy-plugin-complete", "End-to-end plugin deployment: upload DLL, register steps, configure images, and publish. Requires POWERPLATFORM_ENABLE_CUSTOMIZATION=true.", {
2007
+ assemblyPath: z.string().describe("Local DLL file path"),
2008
+ assemblyName: z.string().describe("Assembly name"),
2009
+ stepConfigurations: z.array(z.object({
2010
+ pluginTypeName: z.string(),
2011
+ stepName: z.string(),
2012
+ messageName: z.string(),
2013
+ primaryEntity: z.string(),
2014
+ stage: z.enum(['PreValidation', 'PreOperation', 'PostOperation']),
2015
+ executionMode: z.enum(['Sync', 'Async']),
2016
+ rank: z.number().optional(),
2017
+ filteringAttributes: z.array(z.string()).optional(),
2018
+ preImage: z.object({
2019
+ name: z.string(),
2020
+ alias: z.string(),
2021
+ attributes: z.array(z.string()).optional(),
2022
+ }).optional(),
2023
+ postImage: z.object({
2024
+ name: z.string(),
2025
+ alias: z.string(),
2026
+ attributes: z.array(z.string()).optional(),
2027
+ }).optional(),
2028
+ })).optional().describe("Step configurations (manual registration)"),
2029
+ solutionUniqueName: z.string().optional(),
2030
+ replaceExisting: z.boolean().optional().describe("Update existing assembly vs. create new"),
2031
+ }, async (params) => {
2032
+ try {
2033
+ checkCustomizationEnabled();
2034
+ const service = getPowerPlatformService();
2035
+ const summary = {
2036
+ phases: {
2037
+ deploy: {},
2038
+ register: { stepsCreated: 0, imagesCreated: 0 },
2039
+ },
2040
+ };
2041
+ // Read DLL file
2042
+ const fs = await import('fs/promises');
2043
+ const normalizedPath = params.assemblyPath.replace(/\\/g, '/');
2044
+ const dllBuffer = await fs.readFile(normalizedPath);
2045
+ const dllBase64 = dllBuffer.toString('base64');
2046
+ const version = await service.extractAssemblyVersion(params.assemblyPath);
2047
+ // Phase 1: Deploy assembly
2048
+ if (params.replaceExisting) {
2049
+ // Find existing assembly ID
2050
+ const assemblyId = await service.queryPluginAssemblyByName(params.assemblyName);
2051
+ if (assemblyId) {
2052
+ await service.updatePluginAssembly(assemblyId, dllBase64, version, params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION);
2053
+ summary.phases.deploy = {
2054
+ action: 'updated',
2055
+ assemblyId: assemblyId,
2056
+ version,
2057
+ };
2058
+ }
2059
+ else {
2060
+ throw new Error(`Assembly '${params.assemblyName}' not found for update`);
2061
+ }
2062
+ }
2063
+ else {
2064
+ const uploadResult = await service.createPluginAssembly({
2065
+ name: params.assemblyName,
2066
+ content: dllBase64,
2067
+ version,
2068
+ solutionUniqueName: params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION,
2069
+ });
2070
+ summary.phases.deploy = {
2071
+ action: 'created',
2072
+ assemblyId: uploadResult.pluginAssemblyId,
2073
+ version,
2074
+ pluginTypes: uploadResult.pluginTypes,
2075
+ };
2076
+ }
2077
+ // Phase 2: Register steps
2078
+ if (params.stepConfigurations) {
2079
+ const stageMap = {
2080
+ PreValidation: 10,
2081
+ PreOperation: 20,
2082
+ PostOperation: 40
2083
+ };
2084
+ const modeMap = { Sync: 0, Async: 1 };
2085
+ for (const stepConfig of params.stepConfigurations) {
2086
+ // Resolve plugin type ID
2087
+ const pluginTypeId = await service.queryPluginTypeByTypename(stepConfig.pluginTypeName);
2088
+ // Register step
2089
+ const stepResult = await service.registerPluginStep({
2090
+ pluginTypeId: pluginTypeId,
2091
+ name: stepConfig.stepName,
2092
+ messageName: stepConfig.messageName,
2093
+ primaryEntityName: stepConfig.primaryEntity,
2094
+ stage: stageMap[stepConfig.stage],
2095
+ executionMode: modeMap[stepConfig.executionMode],
2096
+ rank: stepConfig.rank ?? 1,
2097
+ filteringAttributes: stepConfig.filteringAttributes?.join(','),
2098
+ solutionUniqueName: params.solutionUniqueName || POWERPLATFORM_DEFAULT_SOLUTION,
2099
+ });
2100
+ summary.phases.register.stepsCreated++;
2101
+ // Register pre-image
2102
+ if (stepConfig.preImage) {
2103
+ await service.registerPluginImage({
2104
+ stepId: stepResult.stepId,
2105
+ name: stepConfig.preImage.name,
2106
+ imageType: 0,
2107
+ entityAlias: stepConfig.preImage.alias,
2108
+ attributes: stepConfig.preImage.attributes?.join(','),
2109
+ });
2110
+ summary.phases.register.imagesCreated++;
2111
+ }
2112
+ // Register post-image
2113
+ if (stepConfig.postImage) {
2114
+ await service.registerPluginImage({
2115
+ stepId: stepResult.stepId,
2116
+ name: stepConfig.postImage.name,
2117
+ imageType: 1,
2118
+ entityAlias: stepConfig.postImage.alias,
2119
+ attributes: stepConfig.postImage.attributes?.join(','),
2120
+ });
2121
+ summary.phases.register.imagesCreated++;
2122
+ }
2123
+ }
2124
+ }
2125
+ // Phase 3: Publish customizations
2126
+ await service.publishAllCustomizations();
2127
+ summary.phases.publish = { success: true };
2128
+ return {
2129
+ content: [{
2130
+ type: "text",
2131
+ text: `✅ Plugin deployment completed successfully!\n\n` +
2132
+ `📦 Assembly: ${summary.phases.deploy.action === 'created' ? 'Created' : 'Updated'}\n` +
2133
+ `🆔 Assembly ID: ${summary.phases.deploy.assemblyId}\n` +
2134
+ `🔢 Version: ${summary.phases.deploy.version}\n` +
2135
+ `💾 Size: ${(dllBuffer.length / 1024).toFixed(2)} KB\n` +
2136
+ (summary.phases.deploy.pluginTypes ? `🔌 Plugin Types: ${summary.phases.deploy.pluginTypes.length}\n` : '') +
2137
+ `📝 Steps Created: ${summary.phases.register.stepsCreated}\n` +
2138
+ `🖼️ Images Created: ${summary.phases.register.imagesCreated}\n` +
2139
+ `📢 Published: ${summary.phases.publish.success ? 'Yes' : 'No'}\n\n` +
2140
+ `⚡ Deployment is complete and active in the environment!`
2141
+ }]
2142
+ };
2143
+ }
2144
+ catch (error) {
2145
+ console.error("Error deploying plugin:", error);
2146
+ return {
2147
+ content: [{ type: "text", text: `❌ Failed to deploy plugin: ${error.message}` }],
2148
+ isError: true
2149
+ };
2150
+ }
2151
+ });
2152
+ console.error(`✅ powerplatform-customization tools registered (${45} tools)`);
2153
+ }
2154
+ // CLI entry point (standalone execution)
2155
+ if (import.meta.url === `file://${process.argv[1]}`) {
2156
+ const loadEnv = createEnvLoader();
2157
+ loadEnv();
2158
+ const server = createMcpServer({
2159
+ name: '@mcp-consultant-tools/powerplatform-customization',
2160
+ version: '1.0.0',
2161
+ capabilities: { tools: {}, prompts: {} }
2162
+ });
2163
+ registerPowerplatformCustomizationTools(server);
2164
+ const transport = new StdioServerTransport();
2165
+ server.connect(transport).catch((error) => {
2166
+ console.error('Failed to start powerplatform-customization MCP server:', error);
2167
+ process.exit(1);
2168
+ });
2169
+ console.error('powerplatform-customization MCP server running');
2170
+ }
2171
+ //# sourceMappingURL=index.js.map