powerplatform-mcp 0.4.5 → 1.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.
@@ -0,0 +1,288 @@
1
+ /**
2
+ * PluginService
3
+ *
4
+ * Read-only service for plugin assemblies, types, steps, images, and trace logs.
5
+ */
6
+ export class PluginService {
7
+ client;
8
+ constructor(client) {
9
+ this.client = client;
10
+ }
11
+ /**
12
+ * Get all plugin assemblies in the environment
13
+ */
14
+ async getPluginAssemblies(includeManaged = false, maxRecords = 100) {
15
+ const managedFilter = includeManaged ? '' : '$filter=ismanaged eq false&';
16
+ const assemblies = await this.client.get(`api/data/v9.2/pluginassemblies?${managedFilter}$select=pluginassemblyid,name,version,culture,publickeytoken,isolationmode,sourcetype,major,minor,createdon,modifiedon,ismanaged,ishidden&$expand=modifiedby($select=fullname)&$orderby=name&$top=${maxRecords}`);
17
+ // Filter out hidden assemblies and format results
18
+ const formattedAssemblies = assemblies.value
19
+ .filter((assembly) => {
20
+ const isHidden = assembly.ishidden?.Value !== undefined
21
+ ? assembly.ishidden.Value
22
+ : assembly.ishidden;
23
+ return !isHidden;
24
+ })
25
+ .map((assembly) => ({
26
+ pluginassemblyid: assembly.pluginassemblyid,
27
+ name: assembly.name,
28
+ version: assembly.version,
29
+ isolationMode: assembly.isolationmode === 1
30
+ ? 'None'
31
+ : assembly.isolationmode === 2
32
+ ? 'Sandbox'
33
+ : 'External',
34
+ isManaged: assembly.ismanaged,
35
+ modifiedOn: assembly.modifiedon,
36
+ modifiedBy: assembly.modifiedby?.fullname,
37
+ major: assembly.major,
38
+ minor: assembly.minor,
39
+ }));
40
+ return {
41
+ totalCount: formattedAssemblies.length,
42
+ assemblies: formattedAssemblies,
43
+ };
44
+ }
45
+ /**
46
+ * Get a plugin assembly by name with all related plugin types, steps, and images
47
+ */
48
+ async getPluginAssemblyComplete(assemblyName, includeDisabled = false) {
49
+ // Get the plugin assembly
50
+ const assemblies = await this.client.get(`api/data/v9.2/pluginassemblies?$filter=name eq '${assemblyName}'&$select=pluginassemblyid,name,version,culture,publickeytoken,isolationmode,sourcetype,major,minor,createdon,modifiedon,ismanaged,ishidden,description&$expand=modifiedby($select=fullname)`);
51
+ if (!assemblies.value || assemblies.value.length === 0) {
52
+ throw new Error(`Plugin assembly '${assemblyName}' not found`);
53
+ }
54
+ const assembly = assemblies.value[0];
55
+ const assemblyId = assembly.pluginassemblyid;
56
+ // Get plugin types
57
+ const pluginTypes = await this.client.get(`api/data/v9.2/plugintypes?$filter=_pluginassemblyid_value eq ${assemblyId}&$select=plugintypeid,typename,friendlyname,name,assemblyname,description,workflowactivitygroupname`);
58
+ // Get all steps for each plugin type
59
+ const pluginTypeIds = pluginTypes.value.map((pt) => pt.plugintypeid);
60
+ let allSteps = [];
61
+ if (pluginTypeIds.length > 0) {
62
+ const statusFilter = includeDisabled ? '' : ' and statuscode eq 1';
63
+ const typeFilter = pluginTypeIds
64
+ .map((id) => `_plugintypeid_value eq ${id}`)
65
+ .join(' or ');
66
+ const steps = await this.client.get(`api/data/v9.2/sdkmessageprocessingsteps?$filter=(${typeFilter})${statusFilter}&$select=sdkmessageprocessingstepid,name,stage,mode,rank,statuscode,asyncautodelete,filteringattributes,supporteddeployment,configuration,description,invocationsource,_plugintypeid_value,_sdkmessagefilterid_value,_impersonatinguserid_value,_eventhandler_value&$expand=sdkmessageid($select=name),plugintypeid($select=typename),impersonatinguserid($select=fullname),modifiedby($select=fullname),sdkmessagefilterid($select=primaryobjecttypecode)&$orderby=stage,rank`);
67
+ allSteps = steps.value;
68
+ }
69
+ // Get all images for these steps
70
+ const stepIds = allSteps.map((s) => s.sdkmessageprocessingstepid);
71
+ let allImages = [];
72
+ if (stepIds.length > 0) {
73
+ const imageFilter = stepIds
74
+ .map((id) => `_sdkmessageprocessingstepid_value eq ${id}`)
75
+ .join(' or ');
76
+ const images = await this.client.get(`api/data/v9.2/sdkmessageprocessingstepimages?$filter=${imageFilter}&$select=sdkmessageprocessingstepimageid,name,imagetype,messagepropertyname,entityalias,attributes,_sdkmessageprocessingstepid_value`);
77
+ allImages = images.value;
78
+ }
79
+ // Attach images to their respective steps
80
+ const stepsWithImages = allSteps.map((step) => ({
81
+ ...step,
82
+ images: allImages.filter((img) => img._sdkmessageprocessingstepid_value ===
83
+ step.sdkmessageprocessingstepid),
84
+ }));
85
+ // Validation checks
86
+ const validation = {
87
+ hasDisabledSteps: allSteps.some((s) => s.statuscode !== 1),
88
+ hasAsyncSteps: allSteps.some((s) => s.mode === 1),
89
+ hasSyncSteps: allSteps.some((s) => s.mode === 0),
90
+ stepsWithoutFilteringAttributes: stepsWithImages
91
+ .filter((s) => {
92
+ const sdkmsg = s.sdkmessageid;
93
+ const msgName = sdkmsg?.name;
94
+ return ((msgName === 'Update' || msgName === 'Delete') &&
95
+ !s.filteringattributes);
96
+ })
97
+ .map((s) => s.name),
98
+ stepsWithoutImages: stepsWithImages
99
+ .filter((s) => {
100
+ const sdkmsg = s.sdkmessageid;
101
+ const msgName = sdkmsg?.name;
102
+ return (s.images.length === 0 &&
103
+ (msgName === 'Update' || msgName === 'Delete'));
104
+ })
105
+ .map((s) => s.name),
106
+ potentialIssues: [],
107
+ };
108
+ if (validation.stepsWithoutFilteringAttributes.length > 0) {
109
+ validation.potentialIssues.push(`${validation.stepsWithoutFilteringAttributes.length} Update/Delete steps without filtering attributes (performance concern)`);
110
+ }
111
+ if (validation.stepsWithoutImages.length > 0) {
112
+ validation.potentialIssues.push(`${validation.stepsWithoutImages.length} Update/Delete steps without images (may need entity data)`);
113
+ }
114
+ return {
115
+ assembly,
116
+ pluginTypes: pluginTypes.value,
117
+ steps: stepsWithImages,
118
+ validation,
119
+ };
120
+ }
121
+ /**
122
+ * Get all plugins that execute on a specific entity
123
+ */
124
+ async getEntityPluginPipeline(entityName, messageFilter, includeDisabled = false) {
125
+ const statusFilter = includeDisabled ? '' : ' and statuscode eq 1';
126
+ const msgFilter = messageFilter
127
+ ? ` and sdkmessageid/name eq '${messageFilter}'`
128
+ : '';
129
+ const steps = await this.client.get(`api/data/v9.2/sdkmessageprocessingsteps?$filter=sdkmessagefilterid/primaryobjecttypecode eq '${entityName}'${statusFilter}${msgFilter}&$select=sdkmessageprocessingstepid,name,stage,mode,rank,statuscode,asyncautodelete,filteringattributes,supporteddeployment,configuration,description,_plugintypeid_value,_sdkmessagefilterid_value,_impersonatinguserid_value&$expand=sdkmessageid($select=name),plugintypeid($select=typename),impersonatinguserid($select=fullname),sdkmessagefilterid($select=primaryobjecttypecode)&$orderby=stage,rank`);
130
+ // Get assembly information for each plugin type
131
+ const pluginTypeIds = [
132
+ ...new Set(steps.value
133
+ .map((s) => s._plugintypeid_value)
134
+ .filter((id) => id != null)),
135
+ ];
136
+ const assemblyMap = new Map();
137
+ for (const typeId of pluginTypeIds) {
138
+ const pluginType = await this.client.get(`api/data/v9.2/plugintypes(${typeId})?$expand=pluginassemblyid($select=name,version)`);
139
+ assemblyMap.set(typeId, pluginType.pluginassemblyid);
140
+ }
141
+ // Get images for all steps
142
+ const stepIds = steps.value.map((s) => s.sdkmessageprocessingstepid);
143
+ let allImages = [];
144
+ if (stepIds.length > 0) {
145
+ const imageFilter = stepIds
146
+ .map((id) => `_sdkmessageprocessingstepid_value eq ${id}`)
147
+ .join(' or ');
148
+ const images = await this.client.get(`api/data/v9.2/sdkmessageprocessingstepimages?$filter=${imageFilter}&$select=sdkmessageprocessingstepimageid,name,imagetype,messagepropertyname,entityalias,attributes,_sdkmessageprocessingstepid_value`);
149
+ allImages = images.value;
150
+ }
151
+ // Format steps
152
+ const formattedSteps = steps.value.map((step) => {
153
+ const assembly = assemblyMap.get(step._plugintypeid_value);
154
+ const images = allImages.filter((img) => img._sdkmessageprocessingstepid_value ===
155
+ step.sdkmessageprocessingstepid);
156
+ return {
157
+ sdkmessageprocessingstepid: step.sdkmessageprocessingstepid,
158
+ name: step.name,
159
+ stage: step.stage,
160
+ stageName: step.stage === 10
161
+ ? 'PreValidation'
162
+ : step.stage === 20
163
+ ? 'PreOperation'
164
+ : 'PostOperation',
165
+ mode: step.mode,
166
+ modeName: step.mode === 0 ? 'Synchronous' : 'Asynchronous',
167
+ rank: step.rank,
168
+ message: step.sdkmessageid?.name,
169
+ pluginType: step.plugintypeid?.typename,
170
+ assemblyName: assembly?.name,
171
+ assemblyVersion: assembly?.version,
172
+ filteringAttributes: step.filteringattributes
173
+ ? step.filteringattributes.split(',')
174
+ : [],
175
+ statuscode: step.statuscode,
176
+ enabled: step.statuscode === 1,
177
+ deployment: step.supporteddeployment === 0
178
+ ? 'Server'
179
+ : step.supporteddeployment === 1
180
+ ? 'Offline'
181
+ : 'Both',
182
+ impersonatingUser: step.impersonatinguserid
183
+ ?.fullname,
184
+ hasPreImage: images.some((img) => img.imagetype === 0 || img.imagetype === 2),
185
+ hasPostImage: images.some((img) => img.imagetype === 1 || img.imagetype === 2),
186
+ images,
187
+ };
188
+ });
189
+ // Organize by message
190
+ const messageGroups = new Map();
191
+ formattedSteps.forEach((step) => {
192
+ if (!messageGroups.has(step.message)) {
193
+ messageGroups.set(step.message, {
194
+ messageName: step.message,
195
+ stages: {
196
+ preValidation: [],
197
+ preOperation: [],
198
+ postOperation: [],
199
+ },
200
+ });
201
+ }
202
+ const msg = messageGroups.get(step.message);
203
+ if (step.stage === 10)
204
+ msg.stages.preValidation.push(step);
205
+ else if (step.stage === 20)
206
+ msg.stages.preOperation.push(step);
207
+ else if (step.stage === 40)
208
+ msg.stages.postOperation.push(step);
209
+ });
210
+ return {
211
+ entity: entityName,
212
+ messages: Array.from(messageGroups.values()),
213
+ steps: formattedSteps,
214
+ executionOrder: formattedSteps.map((s) => s.name),
215
+ };
216
+ }
217
+ /**
218
+ * Get plugin trace logs with filtering
219
+ */
220
+ async getPluginTraceLogs(options) {
221
+ const { entityName, messageName, correlationId, pluginStepId, exceptionOnly = false, hoursBack = 24, maxRecords = 50, } = options;
222
+ // Build filter
223
+ const filters = [];
224
+ const dateThreshold = new Date();
225
+ dateThreshold.setHours(dateThreshold.getHours() - hoursBack);
226
+ filters.push(`createdon gt ${dateThreshold.toISOString()}`);
227
+ if (entityName)
228
+ filters.push(`primaryentity eq '${entityName}'`);
229
+ if (messageName)
230
+ filters.push(`messagename eq '${messageName}'`);
231
+ if (correlationId)
232
+ filters.push(`correlationid eq '${correlationId}'`);
233
+ if (pluginStepId)
234
+ filters.push(`_sdkmessageprocessingstepid_value eq ${pluginStepId}`);
235
+ if (exceptionOnly)
236
+ filters.push(`exceptiondetails ne null`);
237
+ const filterString = filters.join(' and ');
238
+ const logs = await this.client.get(`api/data/v9.2/plugintracelogs?$filter=${filterString}&$orderby=createdon desc&$top=${maxRecords}`);
239
+ // Parse logs for better readability
240
+ const parsedLogs = logs.value.map((log) => ({
241
+ ...log,
242
+ modeName: log.mode === 0 ? 'Synchronous' : 'Asynchronous',
243
+ operationTypeName: this.getOperationTypeName(log.operationtype),
244
+ parsed: {
245
+ hasException: !!log.exceptiondetails,
246
+ exceptionType: log.exceptiondetails
247
+ ? this.extractExceptionType(log.exceptiondetails)
248
+ : null,
249
+ exceptionMessage: log.exceptiondetails
250
+ ? this.extractExceptionMessage(log.exceptiondetails)
251
+ : null,
252
+ stackTrace: log.exceptiondetails,
253
+ },
254
+ }));
255
+ return {
256
+ totalCount: parsedLogs.length,
257
+ logs: parsedLogs,
258
+ };
259
+ }
260
+ getOperationTypeName(operationType) {
261
+ const types = {
262
+ 0: 'None',
263
+ 1: 'Create',
264
+ 2: 'Update',
265
+ 3: 'Delete',
266
+ 4: 'Retrieve',
267
+ 5: 'RetrieveMultiple',
268
+ 6: 'Associate',
269
+ 7: 'Disassociate',
270
+ };
271
+ return types[operationType] || 'Unknown';
272
+ }
273
+ extractExceptionType(exceptionDetails) {
274
+ const match = exceptionDetails.match(/^([^:]+):/);
275
+ return match ? match[1].trim() : null;
276
+ }
277
+ extractExceptionMessage(exceptionDetails) {
278
+ const lines = exceptionDetails.split('\n');
279
+ if (lines.length > 0) {
280
+ const firstLine = lines[0];
281
+ const colonIndex = firstLine.indexOf(':');
282
+ if (colonIndex > 0) {
283
+ return firstLine.substring(colonIndex + 1).trim();
284
+ }
285
+ }
286
+ return null;
287
+ }
288
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Service for record operations.
3
+ * Handles CRUD operations on Dataverse records.
4
+ */
5
+ export class RecordService {
6
+ client;
7
+ constructor(client) {
8
+ this.client = client;
9
+ }
10
+ /**
11
+ * Get a specific record by entity name (plural) and ID
12
+ * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
13
+ * @param recordId The GUID of the record
14
+ * @returns The record data
15
+ */
16
+ async getRecord(entityNamePlural, recordId) {
17
+ return this.client.get(`api/data/v9.2/${entityNamePlural}(${recordId})`);
18
+ }
19
+ /**
20
+ * Query records using entity name (plural) and a filter expression
21
+ * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
22
+ * @param filter OData filter expression (e.g., "name eq 'test'")
23
+ * @param maxRecords Maximum number of records to retrieve (default: 50)
24
+ * @returns Filtered list of records
25
+ */
26
+ async queryRecords(entityNamePlural, filter, maxRecords = 50) {
27
+ return this.client.get(`api/data/v9.2/${entityNamePlural}?$filter=${encodeURIComponent(filter)}&$top=${maxRecords}`);
28
+ }
29
+ }
@@ -0,0 +1,4 @@
1
+ export { EntityService } from './EntityService.js';
2
+ export { RecordService } from './RecordService.js';
3
+ export { OptionSetService } from './OptionSetService.js';
4
+ export { PluginService } from './PluginService.js';
@@ -0,0 +1,156 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Register entity metadata tools with the MCP server.
4
+ */
5
+ export function registerEntityTools(server, ctx) {
6
+ // Get Entity Metadata
7
+ server.registerTool("get-entity-metadata", {
8
+ title: "Get Entity Metadata",
9
+ description: "Get metadata about a PowerPlatform entity",
10
+ inputSchema: {
11
+ entityName: z.string().describe("The logical name of the entity"),
12
+ },
13
+ outputSchema: z.object({
14
+ entityName: z.string(),
15
+ metadata: z.any(),
16
+ }),
17
+ }, async ({ entityName }) => {
18
+ try {
19
+ const service = ctx.getEntityService();
20
+ const metadata = await service.getEntityMetadata(entityName);
21
+ return {
22
+ structuredContent: { entityName, metadata },
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: `Entity metadata for '${entityName}':\n\n${JSON.stringify(metadata, null, 2)}`,
27
+ },
28
+ ],
29
+ };
30
+ }
31
+ catch (error) {
32
+ console.error("Error getting entity metadata:", error);
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: `Failed to get entity metadata: ${error.message}`,
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ });
43
+ // Get Entity Attributes
44
+ server.registerTool("get-entity-attributes", {
45
+ title: "Get Entity Attributes",
46
+ description: "Get attributes/fields of a PowerPlatform entity",
47
+ inputSchema: {
48
+ entityName: z.string().describe("The logical name of the entity"),
49
+ },
50
+ outputSchema: z.object({
51
+ entityName: z.string(),
52
+ attributes: z.any(),
53
+ }),
54
+ }, async ({ entityName }) => {
55
+ try {
56
+ const service = ctx.getEntityService();
57
+ const attributes = await service.getEntityAttributes(entityName);
58
+ return {
59
+ structuredContent: { entityName, attributes },
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: `Attributes for entity '${entityName}':\n\n${JSON.stringify(attributes, null, 2)}`,
64
+ },
65
+ ],
66
+ };
67
+ }
68
+ catch (error) {
69
+ console.error("Error getting entity attributes:", error);
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: `Failed to get entity attributes: ${error.message}`,
75
+ },
76
+ ],
77
+ };
78
+ }
79
+ });
80
+ // Get Entity Attribute
81
+ server.registerTool("get-entity-attribute", {
82
+ title: "Get Entity Attribute",
83
+ description: "Get a specific attribute/field of a PowerPlatform entity",
84
+ inputSchema: {
85
+ entityName: z.string().describe("The logical name of the entity"),
86
+ attributeName: z.string().describe("The logical name of the attribute"),
87
+ },
88
+ outputSchema: z.object({
89
+ entityName: z.string(),
90
+ attributeName: z.string(),
91
+ attribute: z.any(),
92
+ }),
93
+ }, async ({ entityName, attributeName }) => {
94
+ try {
95
+ const service = ctx.getEntityService();
96
+ const attribute = await service.getEntityAttribute(entityName, attributeName);
97
+ return {
98
+ structuredContent: { entityName, attributeName, attribute },
99
+ content: [
100
+ {
101
+ type: "text",
102
+ text: `Attribute '${attributeName}' for entity '${entityName}':\n\n${JSON.stringify(attribute, null, 2)}`,
103
+ },
104
+ ],
105
+ };
106
+ }
107
+ catch (error) {
108
+ console.error("Error getting entity attribute:", error);
109
+ return {
110
+ content: [
111
+ {
112
+ type: "text",
113
+ text: `Failed to get entity attribute: ${error.message}`,
114
+ },
115
+ ],
116
+ };
117
+ }
118
+ });
119
+ // Get Entity Relationships
120
+ server.registerTool("get-entity-relationships", {
121
+ title: "Get Entity Relationships",
122
+ description: "Get relationships (one-to-many and many-to-many) for a PowerPlatform entity",
123
+ inputSchema: {
124
+ entityName: z.string().describe("The logical name of the entity"),
125
+ },
126
+ outputSchema: z.object({
127
+ entityName: z.string(),
128
+ relationships: z.any(),
129
+ }),
130
+ }, async ({ entityName }) => {
131
+ try {
132
+ const service = ctx.getEntityService();
133
+ const relationships = await service.getEntityRelationships(entityName);
134
+ return {
135
+ structuredContent: { entityName, relationships },
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: `Relationships for entity '${entityName}':\n\n${JSON.stringify(relationships, null, 2)}`,
140
+ },
141
+ ],
142
+ };
143
+ }
144
+ catch (error) {
145
+ console.error("Error getting entity relationships:", error);
146
+ return {
147
+ content: [
148
+ {
149
+ type: "text",
150
+ text: `Failed to get entity relationships: ${error.message}`,
151
+ },
152
+ ],
153
+ };
154
+ }
155
+ });
156
+ }
@@ -0,0 +1,17 @@
1
+ import { registerEntityTools } from "./entityTools.js";
2
+ import { registerRecordTools } from "./recordTools.js";
3
+ import { registerOptionSetTools } from "./optionSetTools.js";
4
+ import { registerPluginTools } from "./pluginTools.js";
5
+ export { registerEntityTools } from "./entityTools.js";
6
+ export { registerRecordTools } from "./recordTools.js";
7
+ export { registerOptionSetTools } from "./optionSetTools.js";
8
+ export { registerPluginTools } from "./pluginTools.js";
9
+ /**
10
+ * Register all tools with the MCP server.
11
+ */
12
+ export function registerAllTools(server, ctx) {
13
+ registerEntityTools(server, ctx);
14
+ registerRecordTools(server, ctx);
15
+ registerOptionSetTools(server, ctx);
16
+ registerPluginTools(server, ctx);
17
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Register option set tools with the MCP server.
4
+ */
5
+ export function registerOptionSetTools(server, ctx) {
6
+ // Get Global Option Set
7
+ server.registerTool("get-global-option-set", {
8
+ title: "Get Global Option Set",
9
+ description: "Get a global option set definition by name",
10
+ inputSchema: {
11
+ optionSetName: z.string().describe("The name of the global option set"),
12
+ },
13
+ outputSchema: z.object({
14
+ optionSetName: z.string(),
15
+ optionSet: z.any(),
16
+ }),
17
+ }, async ({ optionSetName }) => {
18
+ try {
19
+ const service = ctx.getOptionSetService();
20
+ const optionSet = await service.getGlobalOptionSet(optionSetName);
21
+ return {
22
+ structuredContent: { optionSetName, optionSet },
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: `Global option set '${optionSetName}':\n\n${JSON.stringify(optionSet, null, 2)}`,
27
+ },
28
+ ],
29
+ };
30
+ }
31
+ catch (error) {
32
+ console.error("Error getting global option set:", error);
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: `Failed to get global option set: ${error.message}`,
38
+ },
39
+ ],
40
+ };
41
+ }
42
+ });
43
+ }