@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/PowerPlatformService.d.ts +660 -0
- package/build/PowerPlatformService.d.ts.map +1 -0
- package/build/PowerPlatformService.js +2719 -0
- package/build/PowerPlatformService.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2171 -0
- package/build/index.js.map +1 -0
- package/build/utils/bestPractices.d.ts +152 -0
- package/build/utils/bestPractices.d.ts.map +1 -0
- package/build/utils/bestPractices.js +338 -0
- package/build/utils/bestPractices.js.map +1 -0
- package/build/utils/iconManager.d.ts +84 -0
- package/build/utils/iconManager.d.ts.map +1 -0
- package/build/utils/iconManager.js +342 -0
- package/build/utils/iconManager.js.map +1 -0
- package/build/utils/prompt-templates.d.ts +9 -0
- package/build/utils/prompt-templates.d.ts.map +1 -0
- package/build/utils/prompt-templates.js +31 -0
- package/build/utils/prompt-templates.js.map +1 -0
- package/build/utils/rate-limiter.d.ts +108 -0
- package/build/utils/rate-limiter.d.ts.map +1 -0
- package/build/utils/rate-limiter.js +241 -0
- package/build/utils/rate-limiter.js.map +1 -0
- package/package.json +55 -0
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
|