powerplatform-mcp 0.2.0 → 0.4.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/README.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # PowerPlatform MCP Server
2
2
 
3
- A Model Context Protocol (MCP) server for PowerPlatform/Dataverse entities. This tool provides access to entity metadata and attributes in the PowerPlatform environment.
3
+ A Model Context Protocol (MCP) server that provides intelligent access to PowerPlatform/Dataverse entities and records. This tool offers comprehensive entity exploration, metadata access, and AI-assisted query capabilities through GitHub Copilot integration.
4
+
5
+ Key features:
6
+ - Rich entity metadata exploration with formatted, context-aware prompts
7
+ - Advanced OData query support with intelligent filtering
8
+ - Comprehensive relationship mapping and visualization
9
+ - AI-assisted query building and data modeling through AI agent
10
+ - Full access to entity attributes, relationships, and global option sets
11
+
12
+ Perfect for developers working with PowerPlatform/Dataverse who want intelligent, context-aware assistance for entity exploration and data querying.
4
13
 
5
14
  ## Installation
6
15
 
@@ -46,6 +55,306 @@ This is an MCP server designed to work with MCP-compatible clients like GitHub C
46
55
 
47
56
  - `get-entity-metadata`: Get metadata about a PowerPlatform entity
48
57
  - `get-entity-attributes`: Get attributes/fields of a PowerPlatform entity
58
+ - `get-entity-attribute`: Get a specific attribute/field of a PowerPlatform entity
59
+ - `get-entity-relationships`: Get relationships for a PowerPlatform entity
60
+ - `get-global-option-set`: Get a global option set definition
61
+ - `get-record`: Get a specific record by entity name and ID
62
+ - `query-records`: Query records using an OData filter expression
63
+ - `use-powerplatform-prompt`: Use pre-defined prompt templates for PowerPlatform entities
64
+
65
+ ## MCP Prompts
66
+
67
+ The server includes a prompts feature that provides formatted, context-rich information about PowerPlatform entities. These prompts are ideal for use with AI assistants like GitHub Copilot.
68
+
69
+ ### Available Prompt Types
70
+
71
+ The `use-powerplatform-prompt` tool supports the following prompt types:
72
+
73
+ 1. **ENTITY_OVERVIEW**: Comprehensive overview of an entity
74
+ 2. **ATTRIBUTE_DETAILS**: Detailed information about a specific entity attribute
75
+ 3. **QUERY_TEMPLATE**: OData query template for an entity with example filters
76
+ 4. **RELATIONSHIP_MAP**: Visual map of entity relationships
77
+
78
+ ### Examples
79
+
80
+ #### Entity Overview Prompt
81
+
82
+ ```javascript
83
+ // Example client code
84
+ await mcpClient.invoke("use-powerplatform-prompt", {
85
+ promptType: "ENTITY_OVERVIEW",
86
+ entityName: "account"
87
+ });
88
+ ```
89
+
90
+ **Output:**
91
+ ```
92
+ ## Power Platform Entity: account
93
+
94
+ This is an overview of the 'account' entity in Microsoft Power Platform/Dataverse:
95
+
96
+ ### Entity Details
97
+ - Display Name: Account
98
+ - Schema Name: Account
99
+ - Description: Business that represents a customer or potential customer
100
+ - Primary Key: accountid
101
+ - Primary Name: name
102
+
103
+ ### Key Attributes
104
+ - name: String (Account Name)
105
+ - emailaddress1: String (Email)
106
+ - telephone1: String (Main Phone)
107
+ - address1_city: String (City)
108
+ - statecode: Status (Status)
109
+ - accountnumber: String (Account Number)
110
+ - industrycode: OptionSetValue (Industry)
111
+ - revenue: Money (Annual Revenue)
112
+ - ownerid: Owner (Owner)
113
+ - createdon: DateTime (Created On)
114
+
115
+ ### Relationships
116
+ - One-to-Many Relationships: 42
117
+ - Many-to-Many Relationships: 8
118
+
119
+ You can query this entity using OData filters against the plural name.
120
+ ```
121
+
122
+ #### Attribute Details Prompt
123
+
124
+ ```javascript
125
+ await mcpClient.invoke("use-powerplatform-prompt", {
126
+ promptType: "ATTRIBUTE_DETAILS",
127
+ entityName: "account",
128
+ attributeName: "revenue"
129
+ });
130
+ ```
131
+
132
+ **Output:**
133
+ ```
134
+ ## Attribute: revenue
135
+
136
+ Details for the 'revenue' attribute of the 'account' entity:
137
+
138
+ - Display Name: Annual Revenue
139
+ - Description: Annual revenue for the account
140
+ - Type: Money
141
+ - Format: Currency
142
+ - Is Required: No
143
+ - Is Searchable: true
144
+
145
+ ### Usage Notes
146
+ - Data Type: Money
147
+ - Required: No
148
+ - Max Length: N/A
149
+ ```
150
+
151
+ #### Query Template Prompt
152
+
153
+ ```javascript
154
+ await mcpClient.invoke("use-powerplatform-prompt", {
155
+ promptType: "QUERY_TEMPLATE",
156
+ entityName: "account"
157
+ });
158
+ ```
159
+
160
+ **Output:**
161
+ ```
162
+ ## OData Query Template for accounts
163
+
164
+ Use this template to build queries against the accounts entity:
165
+ accounts?$select=name,emailaddress1,telephone1, address1_city,statecode&$filter=name eq 'Example'&$orderby=name asc&$top=50
166
+ ```
167
+
168
+ ### Common Filter Examples
169
+ - Equals: `name eq 'Contoso'`
170
+ - Contains: `contains(name, 'Contoso')`
171
+ - Greater than date: `createdon gt 2023-01-01T00:00:00Z`
172
+ - Multiple conditions: `name eq 'Contoso' and statecode eq 0`
173
+ ```
174
+
175
+ #### Relationship Map Prompt
176
+
177
+ ```javascript
178
+ await mcpClient.invoke("use-powerplatform-prompt", {
179
+ promptType: "RELATIONSHIP_MAP",
180
+ entityName: "contact"
181
+ });
182
+ ```
183
+
184
+ **Output:**
185
+ ```
186
+ ## Relationship Map for contact
187
+
188
+ This shows all relationships for the 'contact' entity:
189
+
190
+ ### One-to-Many Relationships (contact as Primary)
191
+ - contact_activity_parties: contact (1) → activityparty (N)
192
+ - contact_connections1: contact (1) → connection (N)
193
+ - contact_connections2: contact (1) → connection (N)
194
+ - contact_customer_contacts: contact (1) → contact (N)
195
+ - contact_master_contact: contact (1) → contact (N)
196
+
197
+ ### One-to-Many Relationships (contact as Related)
198
+ - account_primary_contact: account (1) → contact (N)
199
+ - customer_contacts: customer (1) → contact (N)
200
+ - system_user_contacts: systemuser (1) → contact (N)
201
+
202
+ ### Many-to-Many Relationships
203
+ - contactleads_association: contact (N) ↔ lead (N)
204
+ - contactopportunities_association: contact (N) ↔ opportunity (N)
205
+ - contactquotes_association: contact (N) ↔ quote (N)
206
+ ```
207
+
208
+ ## Integration with AI Assistants
209
+
210
+ These prompts are particularly valuable when used with AI assistants like GitHub Copilot that can integrate with MCP.
211
+
212
+ ### GitHub Copilot Examples
213
+
214
+ Here are some examples of how you can interact with GitHub Copilot using the PowerPlatform MCP prompts:
215
+
216
+ #### 1. Entity Understanding
217
+
218
+ You can ask GitHub Copilot about entities in natural language:
219
+
220
+ ```
221
+ User: "Tell me about the Account entity in our PowerPlatform environment"
222
+
223
+ GitHub Copilot: Let me fetch the entity overview for you...
224
+ [uses ENTITY_OVERVIEW prompt internally]
225
+
226
+ Here's what I found about the Account entity:
227
+ - It's a core business entity representing customers and potential customers
228
+ - The primary identifier is 'accountid' and the primary name field is 'name'
229
+ - Key fields include:
230
+ * name (Account Name)
231
+ * emailaddress1 (Email)
232
+ * telephone1 (Main Phone)
233
+ * revenue (Annual Revenue)
234
+ - It has 42 one-to-many relationships and 8 many-to-many relationships
235
+ ```
236
+
237
+ #### 2. Building Queries
238
+
239
+ You can ask for help constructing queries:
240
+
241
+ ```
242
+ User: "Help me write a query to find active accounts with revenue over $1M in California"
243
+
244
+ GitHub Copilot: I'll help you build that query...
245
+ [uses QUERY_TEMPLATE prompt internally]
246
+
247
+ Here's the OData query you need:
248
+ accounts?$select=name,revenue,statecode,address1_stateorprovince
249
+ &$filter=statecode eq 0
250
+ and revenue gt 1000000
251
+ and address1_stateorprovince eq 'California'
252
+ &$orderby=revenue desc
253
+
254
+ This query:
255
+ 1. Selects relevant fields
256
+ 2. Filters for active accounts (statecode eq 0)
257
+ 3. Checks revenue > $1M
258
+ 4. Filters for California location
259
+ 5. Orders by revenue highest to lowest
260
+ ```
261
+
262
+ #### 3. Understanding Relationships
263
+
264
+ You can explore entity relationships naturally:
265
+
266
+ ```
267
+ User: "How are Accounts and Contacts related in our system?"
268
+
269
+ GitHub Copilot: Let me check the relationship maps...
270
+ [uses RELATIONSHIP_MAP prompt for both entities internally]
271
+
272
+ Here are the key relationships between Accounts and Contacts:
273
+
274
+ 1. Primary Contact (One-to-Many):
275
+ - account_primary_contact: Account (1) → Contact (N)
276
+ - An account can have many contacts, but one designated as primary
277
+
278
+ 2. Customer Relationship:
279
+ - contact_customer_accounts: Contact (N) ← Account (1)
280
+ - Contacts can be associated with customer accounts
281
+
282
+ 3. Additional Relationships:
283
+ - accountleads_association: For lead tracking
284
+ - account_connections: For relationship management
285
+ ```
286
+
287
+ #### 4. Field/Attribute Investigation
288
+
289
+ You can ask about specific fields:
290
+
291
+ ```
292
+ User: "What kind of field is the revenue field on Account?"
293
+
294
+ GitHub Copilot: Let me check the attribute details...
295
+ [uses ATTRIBUTE_DETAILS prompt internally]
296
+
297
+ The revenue field on Account is:
298
+ - Data Type: Money
299
+ - Format: Currency
300
+ - Not required
301
+ - Searchable: Yes
302
+ - Used for: Tracking annual revenue for the account
303
+ - Can be used in advanced find and filtered views
304
+ ```
305
+
306
+ #### 5. Complex Data Modeling
307
+
308
+ You can get help with data modeling decisions:
309
+
310
+ ```
311
+ User: "I need to design a solution for tracking product reviews. Which entities should I relate to?"
312
+
313
+ GitHub Copilot: Let me analyze the relevant entities...
314
+ [uses multiple ENTITY_OVERVIEW and RELATIONSHIP_MAP prompts internally]
315
+
316
+ Based on the existing data model, here's a suggested approach:
317
+
318
+ 1. Product (existing entity):
319
+ - Already has relationships with:
320
+ * Price lists
321
+ * Product categories
322
+ * Inventory units
323
+
324
+ 2. New Review entity should relate to:
325
+ - Product (N:1) - Each review is for one product
326
+ - Contact (N:1) - Reviews are written by contacts
327
+ - Account (N:1) - Optional link to company if B2B review
328
+
329
+ Here's a sample lookup field structure...
330
+ [continues with detailed implementation guidance]
331
+ ```
332
+
333
+ #### 6. Debugging Assistance
334
+
335
+ You can get help troubleshooting:
336
+
337
+ ```
338
+ User: "Why am I getting 'invalid property' when querying account.customertypecode?"
339
+
340
+ GitHub Copilot: Let me check the attribute details...
341
+ [uses ATTRIBUTE_DETAILS prompt internally]
342
+
343
+ I see the issue. The field 'customertypecode' is actually named 'customertype' in your environment. Here's the correct field information:
344
+ - Logical Name: customertype
345
+ - Type: OptionSetValue
346
+ - Valid Values:
347
+ * 1 = Competitor
348
+ * 2 = Consultant
349
+ * 3 = Customer
350
+ * 4 = Investor
351
+ * 5 = Partner
352
+ * 6 = Influencer
353
+
354
+ Try updating your query to use 'customertype' instead of 'customertypecode'.
355
+ ```
356
+
357
+ These examples show how GitHub Copilot can leverage the MCP prompts to provide context-aware, accurate assistance for PowerPlatform development tasks. The AI understands your environment's specific configuration and can help with both simple queries and complex architectural decisions.
49
358
 
50
359
  ## License
51
360
 
@@ -80,6 +80,95 @@ export class PowerPlatformService {
80
80
  * @param entityName The logical name of the entity
81
81
  */
82
82
  async getEntityAttributes(entityName) {
83
- return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes`);
83
+ const selectProperties = [
84
+ 'AttributeType',
85
+ 'EntityLogicalName',
86
+ 'LogicalName',
87
+ 'SchemaName'
88
+ ].join(',');
89
+ return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes?$select=${selectProperties}`);
90
+ }
91
+ /**
92
+ * Get metadata about a specific entity attribute/field
93
+ * @param entityName The logical name of the entity
94
+ * @param attributeName The logical name of the attribute
95
+ */
96
+ async getEntityAttribute(entityName, attributeName) {
97
+ return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes(LogicalName='${attributeName}')`);
98
+ }
99
+ /**
100
+ * Get one-to-many relationships for an entity
101
+ * @param entityName The logical name of the entity
102
+ */
103
+ async getEntityOneToManyRelationships(entityName) {
104
+ const selectProperties = [
105
+ 'SchemaName',
106
+ 'RelationshipType',
107
+ 'ReferencedAttribute',
108
+ 'ReferencedEntity',
109
+ 'ReferencingAttribute',
110
+ 'ReferencingEntity',
111
+ 'ReferencedEntityNavigationPropertyName',
112
+ 'ReferencingEntityNavigationPropertyName'
113
+ ].join(',');
114
+ return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/OneToManyRelationships?$select=${selectProperties}`);
115
+ }
116
+ /**
117
+ * Get many-to-many relationships for an entity
118
+ * @param entityName The logical name of the entity
119
+ */
120
+ async getEntityManyToManyRelationships(entityName) {
121
+ const selectProperties = [
122
+ 'SchemaName',
123
+ 'RelationshipType',
124
+ 'Entity1LogicalName',
125
+ 'Entity2LogicalName',
126
+ 'Entity1IntersectAttribute',
127
+ 'Entity2IntersectAttribute',
128
+ 'Entity1NavigationPropertyName',
129
+ 'Entity2NavigationPropertyName'
130
+ ].join(',');
131
+ return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/ManyToManyRelationships?$select=${selectProperties}`);
132
+ }
133
+ /**
134
+ * Get all relationships (one-to-many and many-to-many) for an entity
135
+ * @param entityName The logical name of the entity
136
+ */
137
+ async getEntityRelationships(entityName) {
138
+ const [oneToMany, manyToMany] = await Promise.all([
139
+ this.getEntityOneToManyRelationships(entityName),
140
+ this.getEntityManyToManyRelationships(entityName)
141
+ ]);
142
+ return {
143
+ oneToMany,
144
+ manyToMany
145
+ };
146
+ }
147
+ /**
148
+ * Get a global option set definition by name
149
+ * @param optionSetName The name of the global option set
150
+ * @returns The global option set definition
151
+ */
152
+ async getGlobalOptionSet(optionSetName) {
153
+ return this.makeRequest(`api/data/v9.2/GlobalOptionSetDefinitions(Name='${optionSetName}')`);
154
+ }
155
+ /**
156
+ * Get a specific record by entity name (plural) and ID
157
+ * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
158
+ * @param recordId The GUID of the record
159
+ * @returns The record data
160
+ */
161
+ async getRecord(entityNamePlural, recordId) {
162
+ return this.makeRequest(`api/data/v9.2/${entityNamePlural}(${recordId})`);
163
+ }
164
+ /**
165
+ * Query records using entity name (plural) and a filter expression
166
+ * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
167
+ * @param filter OData filter expression (e.g., "name eq 'test'")
168
+ * @param maxRecords Maximum number of records to retrieve (default: 50)
169
+ * @returns Filtered list of records
170
+ */
171
+ async queryRecords(entityNamePlural, filter, maxRecords = 50) {
172
+ return this.makeRequest(`api/data/v9.2/${entityNamePlural}?$filter=${encodeURIComponent(filter)}&$top=${maxRecords}`);
84
173
  }
85
174
  }
package/build/index.js CHANGED
@@ -39,6 +39,258 @@ function getPowerPlatformService() {
39
39
  }
40
40
  return powerPlatformService;
41
41
  }
42
+ // Pre-defined PowerPlatform Prompts
43
+ const powerPlatformPrompts = {
44
+ // Entity exploration prompts
45
+ ENTITY_OVERVIEW: (entityName) => `## Power Platform Entity: ${entityName}\n\n` +
46
+ `This is an overview of the '${entityName}' entity in Microsoft Power Platform/Dataverse:\n\n` +
47
+ `### Entity Details\n{{entity_details}}\n\n` +
48
+ `### Attributes\n{{key_attributes}}\n\n` +
49
+ `### Relationships\n{{relationships}}\n\n` +
50
+ `You can query this entity using OData filters against the plural name.`,
51
+ ATTRIBUTE_DETAILS: (entityName, attributeName) => `## Attribute: ${attributeName}\n\n` +
52
+ `Details for the '${attributeName}' attribute of the '${entityName}' entity:\n\n` +
53
+ `{{attribute_details}}\n\n` +
54
+ `### Usage Notes\n` +
55
+ `- Data Type: {{data_type}}\n` +
56
+ `- Required: {{required}}\n` +
57
+ `- Max Length: {{max_length}}`,
58
+ // Query builder prompts
59
+ QUERY_TEMPLATE: (entityNamePlural) => `## OData Query Template for ${entityNamePlural}\n\n` +
60
+ `Use this template to build queries against the ${entityNamePlural} entity:\n\n` +
61
+ `\`\`\`\n${entityNamePlural}?$select={{selected_fields}}&$filter={{filter_conditions}}&$orderby={{order_by}}&$top={{max_records}}\n\`\`\`\n\n` +
62
+ `### Common Filter Examples\n` +
63
+ `- Equals: \`name eq 'Contoso'\`\n` +
64
+ `- Contains: \`contains(name, 'Contoso')\`\n` +
65
+ `- Greater than date: \`createdon gt 2023-01-01T00:00:00Z\`\n` +
66
+ `- Multiple conditions: \`name eq 'Contoso' and statecode eq 0\``,
67
+ // Relationship exploration prompts
68
+ RELATIONSHIP_MAP: (entityName) => `## Relationship Map for ${entityName}\n\n` +
69
+ `This shows all relationships for the '${entityName}' entity:\n\n` +
70
+ `### One-to-Many Relationships (${entityName} as Primary)\n{{one_to_many_primary}}\n\n` +
71
+ `### One-to-Many Relationships (${entityName} as Related)\n{{one_to_many_related}}\n\n` +
72
+ `### Many-to-Many Relationships\n{{many_to_many}}\n\n`
73
+ };
74
+ // Register prompts with the server using the correct method signature
75
+ // Entity Overview Prompt
76
+ server.prompt("entity-overview", "Get an overview of a Power Platform entity", {
77
+ entityName: z.string().describe("The logical name of the entity")
78
+ }, async (args) => {
79
+ try {
80
+ const service = getPowerPlatformService();
81
+ const entityName = args.entityName;
82
+ // Get entity metadata and key attributes
83
+ const [metadata, attributes] = await Promise.all([
84
+ service.getEntityMetadata(entityName),
85
+ service.getEntityAttributes(entityName)
86
+ ]);
87
+ // Format entity details
88
+ const entityDetails = `- Display Name: ${metadata.DisplayName?.UserLocalizedLabel?.Label || entityName}\n` +
89
+ `- Schema Name: ${metadata.SchemaName}\n` +
90
+ `- Description: ${metadata.Description?.UserLocalizedLabel?.Label || 'No description'}\n` +
91
+ `- Primary Key: ${metadata.PrimaryIdAttribute}\n` +
92
+ `- Primary Name: ${metadata.PrimaryNameAttribute}`;
93
+ // Get key attributes
94
+ const keyAttributes = attributes.value
95
+ .filter((attr) => attr.IsValidForRead === true && !attr.AttributeOf)
96
+ .map((attr) => `- ${attr.LogicalName}: ${attr.AttributeType} (${attr.DisplayName?.UserLocalizedLabel?.Label || 'No display name'})`)
97
+ .join('\n');
98
+ // Get relationships summary
99
+ const relationships = await service.getEntityRelationships(entityName);
100
+ const oneToManyCount = relationships.oneToMany.value.length;
101
+ const manyToManyCount = relationships.manyToMany.value.length;
102
+ const relationshipsSummary = `- One-to-Many Relationships: ${oneToManyCount}\n` +
103
+ `- Many-to-Many Relationships: ${manyToManyCount}`;
104
+ let promptContent = powerPlatformPrompts.ENTITY_OVERVIEW(entityName);
105
+ promptContent = promptContent
106
+ .replace('{{entity_details}}', entityDetails)
107
+ .replace('{{key_attributes}}', keyAttributes)
108
+ .replace('{{relationships}}', relationshipsSummary);
109
+ return {
110
+ messages: [
111
+ {
112
+ role: "assistant",
113
+ content: {
114
+ type: "text",
115
+ text: promptContent
116
+ }
117
+ }
118
+ ]
119
+ };
120
+ }
121
+ catch (error) {
122
+ console.error(`Error handling entity-overview prompt:`, error);
123
+ return {
124
+ messages: [
125
+ {
126
+ role: "assistant",
127
+ content: {
128
+ type: "text",
129
+ text: `Error: ${error.message}`
130
+ }
131
+ }
132
+ ]
133
+ };
134
+ }
135
+ });
136
+ // Attribute Details Prompt
137
+ server.prompt("attribute-details", "Get detailed information about a specific entity attribute/field", {
138
+ entityName: z.string().describe("The logical name of the entity"),
139
+ attributeName: z.string().describe("The logical name of the attribute"),
140
+ }, async (args) => {
141
+ try {
142
+ const service = getPowerPlatformService();
143
+ const { entityName, attributeName } = args;
144
+ // Get attribute details
145
+ const attribute = await service.getEntityAttribute(entityName, attributeName);
146
+ // Format attribute details
147
+ const attrDetails = `- Display Name: ${attribute.DisplayName?.UserLocalizedLabel?.Label || attributeName}\n` +
148
+ `- Description: ${attribute.Description?.UserLocalizedLabel?.Label || 'No description'}\n` +
149
+ `- Type: ${attribute.AttributeType}\n` +
150
+ `- Format: ${attribute.Format || 'N/A'}\n` +
151
+ `- Is Required: ${attribute.RequiredLevel?.Value || 'No'}\n` +
152
+ `- Is Searchable: ${attribute.IsValidForAdvancedFind || false}`;
153
+ let promptContent = powerPlatformPrompts.ATTRIBUTE_DETAILS(entityName, attributeName);
154
+ promptContent = promptContent
155
+ .replace('{{attribute_details}}', attrDetails)
156
+ .replace('{{data_type}}', attribute.AttributeType)
157
+ .replace('{{required}}', attribute.RequiredLevel?.Value || 'No')
158
+ .replace('{{max_length}}', attribute.MaxLength || 'N/A');
159
+ return {
160
+ messages: [
161
+ {
162
+ role: "assistant",
163
+ content: {
164
+ type: "text",
165
+ text: promptContent
166
+ }
167
+ }
168
+ ]
169
+ };
170
+ }
171
+ catch (error) {
172
+ console.error(`Error handling attribute-details prompt:`, error);
173
+ return {
174
+ messages: [
175
+ {
176
+ role: "assistant",
177
+ content: {
178
+ type: "text",
179
+ text: `Error: ${error.message}`
180
+ }
181
+ }
182
+ ]
183
+ };
184
+ }
185
+ });
186
+ // Query Template Prompt
187
+ server.prompt("query-template", "Get a template for querying a Power Platform entity", {
188
+ entityName: z.string().describe("The logical name of the entity"),
189
+ }, async (args) => {
190
+ try {
191
+ const service = getPowerPlatformService();
192
+ const entityName = args.entityName;
193
+ // Get entity metadata to determine plural name
194
+ const metadata = await service.getEntityMetadata(entityName);
195
+ const entityNamePlural = metadata.EntitySetName;
196
+ // Get a few important fields for the select example
197
+ const attributes = await service.getEntityAttributes(entityName);
198
+ const selectFields = attributes.value
199
+ .filter((attr) => attr.IsValidForRead === true && !attr.AttributeOf)
200
+ .slice(0, 5) // Just take first 5 for example
201
+ .map((attr) => attr.LogicalName)
202
+ .join(',');
203
+ let promptContent = powerPlatformPrompts.QUERY_TEMPLATE(entityNamePlural);
204
+ promptContent = promptContent
205
+ .replace('{{selected_fields}}', selectFields)
206
+ .replace('{{filter_conditions}}', `${metadata.PrimaryNameAttribute} eq 'Example'`)
207
+ .replace('{{order_by}}', `${metadata.PrimaryNameAttribute} asc`)
208
+ .replace('{{max_records}}', '50');
209
+ return {
210
+ messages: [
211
+ {
212
+ role: "assistant",
213
+ content: {
214
+ type: "text",
215
+ text: promptContent
216
+ }
217
+ }
218
+ ]
219
+ };
220
+ }
221
+ catch (error) {
222
+ console.error(`Error handling query-template prompt:`, error);
223
+ return {
224
+ messages: [
225
+ {
226
+ role: "assistant",
227
+ content: {
228
+ type: "text",
229
+ text: `Error: ${error.message}`
230
+ }
231
+ }
232
+ ]
233
+ };
234
+ }
235
+ });
236
+ // Relationship Map Prompt
237
+ server.prompt("relationship-map", "Get a list of relationships for a Power Platform entity", {
238
+ entityName: z.string().describe("The logical name of the entity"),
239
+ }, async (args) => {
240
+ try {
241
+ const service = getPowerPlatformService();
242
+ const entityName = args.entityName;
243
+ // Get relationships
244
+ const relationships = await service.getEntityRelationships(entityName);
245
+ // Format one-to-many relationships where this entity is primary
246
+ const oneToManyPrimary = relationships.oneToMany.value
247
+ .filter((rel) => rel.ReferencingEntity !== entityName)
248
+ .map((rel) => `- ${rel.SchemaName}: ${entityName} (1) → ${rel.ReferencingEntity} (N)`)
249
+ .join('\n');
250
+ // Format one-to-many relationships where this entity is related
251
+ const oneToManyRelated = relationships.oneToMany.value
252
+ .filter((rel) => rel.ReferencingEntity === entityName)
253
+ .map((rel) => `- ${rel.SchemaName}: ${rel.ReferencedEntity} (1) → ${entityName} (N)`)
254
+ .join('\n');
255
+ // Format many-to-many relationships
256
+ const manyToMany = relationships.manyToMany.value
257
+ .map((rel) => {
258
+ const otherEntity = rel.Entity1LogicalName === entityName ? rel.Entity2LogicalName : rel.Entity1LogicalName;
259
+ return `- ${rel.SchemaName}: ${entityName} (N) ↔ ${otherEntity} (N)`;
260
+ })
261
+ .join('\n');
262
+ let promptContent = powerPlatformPrompts.RELATIONSHIP_MAP(entityName);
263
+ promptContent = promptContent
264
+ .replace('{{one_to_many_primary}}', oneToManyPrimary || 'None found')
265
+ .replace('{{one_to_many_related}}', oneToManyRelated || 'None found')
266
+ .replace('{{many_to_many}}', manyToMany || 'None found');
267
+ return {
268
+ messages: [
269
+ {
270
+ role: "assistant",
271
+ content: {
272
+ type: "text",
273
+ text: promptContent
274
+ }
275
+ }
276
+ ]
277
+ };
278
+ }
279
+ catch (error) {
280
+ console.error(`Error handling relationship-map prompt:`, error);
281
+ return {
282
+ messages: [
283
+ {
284
+ role: "assistant",
285
+ content: {
286
+ type: "text",
287
+ text: `Error: ${error.message}`
288
+ }
289
+ }
290
+ ]
291
+ };
292
+ }
293
+ });
42
294
  // PowerPlatform entity metadata
43
295
  server.tool("get-entity-metadata", "Get metadata about a PowerPlatform entity", {
44
296
  entityName: z.string().describe("The logical name of the entity"),
@@ -64,7 +316,7 @@ server.tool("get-entity-metadata", "Get metadata about a PowerPlatform entity",
64
316
  content: [
65
317
  {
66
318
  type: "text",
67
- text: `Failed to get entity metadata: ${error}`,
319
+ text: `Failed to get entity metadata: ${error.message}`,
68
320
  },
69
321
  ],
70
322
  };
@@ -95,7 +347,315 @@ server.tool("get-entity-attributes", "Get attributes/fields of a PowerPlatform e
95
347
  content: [
96
348
  {
97
349
  type: "text",
98
- text: `Failed to get entity attributes: ${error}`,
350
+ text: `Failed to get entity attributes: ${error.message}`,
351
+ },
352
+ ],
353
+ };
354
+ }
355
+ });
356
+ // PowerPlatform specific entity attribute
357
+ server.tool("get-entity-attribute", "Get a specific attribute/field of a PowerPlatform entity", {
358
+ entityName: z.string().describe("The logical name of the entity"),
359
+ attributeName: z.string().describe("The logical name of the attribute")
360
+ }, async ({ entityName, attributeName }) => {
361
+ try {
362
+ // Get or initialize PowerPlatformService
363
+ const service = getPowerPlatformService();
364
+ const attribute = await service.getEntityAttribute(entityName, attributeName);
365
+ // Format the attribute as a string for text display
366
+ const attributeStr = JSON.stringify(attribute, null, 2);
367
+ return {
368
+ content: [
369
+ {
370
+ type: "text",
371
+ text: `Attribute '${attributeName}' for entity '${entityName}':\n\n${attributeStr}`,
372
+ },
373
+ ],
374
+ };
375
+ }
376
+ catch (error) {
377
+ console.error("Error getting entity attribute:", error);
378
+ return {
379
+ content: [
380
+ {
381
+ type: "text",
382
+ text: `Failed to get entity attribute: ${error.message}`,
383
+ },
384
+ ],
385
+ };
386
+ }
387
+ });
388
+ // PowerPlatform entity relationships
389
+ server.tool("get-entity-relationships", "Get relationships (one-to-many and many-to-many) for a PowerPlatform entity", {
390
+ entityName: z.string().describe("The logical name of the entity"),
391
+ }, async ({ entityName }) => {
392
+ try {
393
+ // Get or initialize PowerPlatformService
394
+ const service = getPowerPlatformService();
395
+ const relationships = await service.getEntityRelationships(entityName);
396
+ // Format the relationships as a string for text display
397
+ const relationshipsStr = JSON.stringify(relationships, null, 2);
398
+ return {
399
+ content: [
400
+ {
401
+ type: "text",
402
+ text: `Relationships for entity '${entityName}':\n\n${relationshipsStr}`,
403
+ },
404
+ ],
405
+ };
406
+ }
407
+ catch (error) {
408
+ console.error("Error getting entity relationships:", error);
409
+ return {
410
+ content: [
411
+ {
412
+ type: "text",
413
+ text: `Failed to get entity relationships: ${error.message}`,
414
+ },
415
+ ],
416
+ };
417
+ }
418
+ });
419
+ // PowerPlatform global option set
420
+ server.tool("get-global-option-set", "Get a global option set definition by name", {
421
+ optionSetName: z.string().describe("The name of the global option set"),
422
+ }, async ({ optionSetName }) => {
423
+ try {
424
+ // Get or initialize PowerPlatformService
425
+ const service = getPowerPlatformService();
426
+ const optionSet = await service.getGlobalOptionSet(optionSetName);
427
+ // Format the option set as a string for text display
428
+ const optionSetStr = JSON.stringify(optionSet, null, 2);
429
+ return {
430
+ content: [
431
+ {
432
+ type: "text",
433
+ text: `Global option set '${optionSetName}':\n\n${optionSetStr}`,
434
+ },
435
+ ],
436
+ };
437
+ }
438
+ catch (error) {
439
+ console.error("Error getting global option set:", error);
440
+ return {
441
+ content: [
442
+ {
443
+ type: "text",
444
+ text: `Failed to get global option set: ${error.message}`,
445
+ },
446
+ ],
447
+ };
448
+ }
449
+ });
450
+ // PowerPlatform record by ID
451
+ server.tool("get-record", "Get a specific record by entity name (plural) and ID", {
452
+ entityNamePlural: z.string().describe("The plural name of the entity (e.g., 'accounts', 'contacts')"),
453
+ recordId: z.string().describe("The GUID of the record"),
454
+ }, async ({ entityNamePlural, recordId }) => {
455
+ try {
456
+ // Get or initialize PowerPlatformService
457
+ const service = getPowerPlatformService();
458
+ const record = await service.getRecord(entityNamePlural, recordId);
459
+ // Format the record as a string for text display
460
+ const recordStr = JSON.stringify(record, null, 2);
461
+ return {
462
+ content: [
463
+ {
464
+ type: "text",
465
+ text: `Record from '${entityNamePlural}' with ID '${recordId}':\n\n${recordStr}`,
466
+ },
467
+ ],
468
+ };
469
+ }
470
+ catch (error) {
471
+ console.error("Error getting record:", error);
472
+ return {
473
+ content: [
474
+ {
475
+ type: "text",
476
+ text: `Failed to get record: ${error.message}`,
477
+ },
478
+ ],
479
+ };
480
+ }
481
+ });
482
+ // PowerPlatform query records with filter
483
+ server.tool("query-records", "Query records using an OData filter expression", {
484
+ entityNamePlural: z.string().describe("The plural name of the entity (e.g., 'accounts', 'contacts')"),
485
+ filter: z.string().describe("OData filter expression (e.g., \"name eq 'test'\" or \"createdon gt 2023-01-01\")"),
486
+ maxRecords: z.number().optional().describe("Maximum number of records to retrieve (default: 50)"),
487
+ }, async ({ entityNamePlural, filter, maxRecords }) => {
488
+ try {
489
+ // Get or initialize PowerPlatformService
490
+ const service = getPowerPlatformService();
491
+ const records = await service.queryRecords(entityNamePlural, filter, maxRecords || 50);
492
+ // Format the records as a string for text display
493
+ const recordsStr = JSON.stringify(records, null, 2);
494
+ const recordCount = records.value?.length || 0;
495
+ return {
496
+ content: [
497
+ {
498
+ type: "text",
499
+ text: `Retrieved ${recordCount} records from '${entityNamePlural}' with filter '${filter}':\n\n${recordsStr}`,
500
+ },
501
+ ],
502
+ };
503
+ }
504
+ catch (error) {
505
+ console.error("Error querying records:", error);
506
+ return {
507
+ content: [
508
+ {
509
+ type: "text",
510
+ text: `Failed to query records: ${error.message}`,
511
+ },
512
+ ],
513
+ };
514
+ }
515
+ });
516
+ // PowerPlatform MCP Prompts
517
+ server.tool("use-powerplatform-prompt", "Use a predefined prompt template for PowerPlatform entities", {
518
+ promptType: z.enum([
519
+ "ENTITY_OVERVIEW",
520
+ "ATTRIBUTE_DETAILS",
521
+ "QUERY_TEMPLATE",
522
+ "RELATIONSHIP_MAP"
523
+ ]).describe("The type of prompt template to use"),
524
+ entityName: z.string().describe("The logical name of the entity"),
525
+ attributeName: z.string().optional().describe("The logical name of the attribute (required for ATTRIBUTE_DETAILS prompt)"),
526
+ }, async ({ promptType, entityName, attributeName }) => {
527
+ try {
528
+ // Get or initialize PowerPlatformService
529
+ const service = getPowerPlatformService();
530
+ let promptContent = "";
531
+ let replacements = {};
532
+ switch (promptType) {
533
+ case "ENTITY_OVERVIEW": {
534
+ // Get entity metadata and key attributes
535
+ const [metadata, attributes] = await Promise.all([
536
+ service.getEntityMetadata(entityName),
537
+ service.getEntityAttributes(entityName)
538
+ ]);
539
+ // Format entity details
540
+ const entityDetails = `- Display Name: ${metadata.DisplayName?.UserLocalizedLabel?.Label || entityName}\n` +
541
+ `- Schema Name: ${metadata.SchemaName}\n` +
542
+ `- Description: ${metadata.Description?.UserLocalizedLabel?.Label || 'No description'}\n` +
543
+ `- Primary Key: ${metadata.PrimaryIdAttribute}\n` +
544
+ `- Primary Name: ${metadata.PrimaryNameAttribute}`;
545
+ // Get key attributes
546
+ const keyAttributes = attributes.value
547
+ .filter((attr) => attr.IsValidForRead === true && !attr.AttributeOf)
548
+ //.slice(0, 10) // Limit to first 10 important attributes
549
+ .map((attr) => `- ${attr.LogicalName}: ${attr.AttributeType} (${attr.DisplayName?.UserLocalizedLabel?.Label || 'No display name'})`)
550
+ .join('\n');
551
+ // Get relationships summary
552
+ const relationships = await service.getEntityRelationships(entityName);
553
+ const oneToManyCount = relationships.oneToMany.value.length;
554
+ const manyToManyCount = relationships.manyToMany.value.length;
555
+ const relationshipsSummary = `- One-to-Many Relationships: ${oneToManyCount}\n` +
556
+ `- Many-to-Many Relationships: ${manyToManyCount}`;
557
+ promptContent = powerPlatformPrompts.ENTITY_OVERVIEW(entityName);
558
+ replacements = {
559
+ '{{entity_details}}': entityDetails,
560
+ '{{key_attributes}}': keyAttributes,
561
+ '{{relationships}}': relationshipsSummary
562
+ };
563
+ break;
564
+ }
565
+ case "ATTRIBUTE_DETAILS": {
566
+ if (!attributeName) {
567
+ throw new Error("attributeName is required for ATTRIBUTE_DETAILS prompt");
568
+ }
569
+ // Get attribute details
570
+ const attribute = await service.getEntityAttribute(entityName, attributeName);
571
+ // Format attribute details
572
+ const attrDetails = `- Display Name: ${attribute.DisplayName?.UserLocalizedLabel?.Label || attributeName}\n` +
573
+ `- Description: ${attribute.Description?.UserLocalizedLabel?.Label || 'No description'}\n` +
574
+ `- Type: ${attribute.AttributeType}\n` +
575
+ `- Format: ${attribute.Format || 'N/A'}\n` +
576
+ `- Is Required: ${attribute.RequiredLevel?.Value || 'No'}\n` +
577
+ `- Is Searchable: ${attribute.IsValidForAdvancedFind || false}`;
578
+ promptContent = powerPlatformPrompts.ATTRIBUTE_DETAILS(entityName, attributeName);
579
+ replacements = {
580
+ '{{attribute_details}}': attrDetails,
581
+ '{{data_type}}': attribute.AttributeType,
582
+ '{{required}}': attribute.RequiredLevel?.Value || 'No',
583
+ '{{max_length}}': attribute.MaxLength || 'N/A'
584
+ };
585
+ break;
586
+ }
587
+ case "QUERY_TEMPLATE": {
588
+ // Get entity metadata to determine plural name
589
+ const metadata = await service.getEntityMetadata(entityName);
590
+ const entityNamePlural = metadata.EntitySetName;
591
+ // Get a few important fields for the select example
592
+ const attributes = await service.getEntityAttributes(entityName);
593
+ const selectFields = attributes.value
594
+ .filter((attr) => attr.IsValidForRead === true && !attr.AttributeOf)
595
+ .slice(0, 5) // Just take first 5 for example
596
+ .map((attr) => attr.LogicalName)
597
+ .join(',');
598
+ promptContent = powerPlatformPrompts.QUERY_TEMPLATE(entityNamePlural);
599
+ replacements = {
600
+ '{{selected_fields}}': selectFields,
601
+ '{{filter_conditions}}': `${metadata.PrimaryNameAttribute} eq 'Example'`,
602
+ '{{order_by}}': `${metadata.PrimaryNameAttribute} asc`,
603
+ '{{max_records}}': '50'
604
+ };
605
+ break;
606
+ }
607
+ case "RELATIONSHIP_MAP": {
608
+ // Get relationships
609
+ const relationships = await service.getEntityRelationships(entityName);
610
+ // Format one-to-many relationships where this entity is primary
611
+ const oneToManyPrimary = relationships.oneToMany.value
612
+ .filter((rel) => rel.ReferencingEntity !== entityName)
613
+ //.slice(0, 10) // Limit to 10 for readability
614
+ .map((rel) => `- ${rel.SchemaName}: ${entityName} (1) → ${rel.ReferencingEntity} (N)`)
615
+ .join('\n');
616
+ // Format one-to-many relationships where this entity is related
617
+ const oneToManyRelated = relationships.oneToMany.value
618
+ .filter((rel) => rel.ReferencingEntity === entityName)
619
+ //.slice(0, 10) // Limit to 10 for readability
620
+ .map((rel) => `- ${rel.SchemaName}: ${rel.ReferencedEntity} (1) → ${entityName} (N)`)
621
+ .join('\n');
622
+ // Format many-to-many relationships
623
+ const manyToMany = relationships.manyToMany.value
624
+ //.slice(0, 10) // Limit to 10 for readability
625
+ .map((rel) => {
626
+ const otherEntity = rel.Entity1LogicalName === entityName ? rel.Entity2LogicalName : rel.Entity1LogicalName;
627
+ return `- ${rel.SchemaName}: ${entityName} (N) ↔ ${otherEntity} (N)`;
628
+ })
629
+ .join('\n');
630
+ promptContent = powerPlatformPrompts.RELATIONSHIP_MAP(entityName);
631
+ replacements = {
632
+ '{{one_to_many_primary}}': oneToManyPrimary || 'None found',
633
+ '{{one_to_many_related}}': oneToManyRelated || 'None found',
634
+ '{{many_to_many}}': manyToMany || 'None found'
635
+ };
636
+ break;
637
+ }
638
+ }
639
+ // Replace all placeholders in the template
640
+ for (const [placeholder, value] of Object.entries(replacements)) {
641
+ promptContent = promptContent.replace(placeholder, value);
642
+ }
643
+ return {
644
+ content: [
645
+ {
646
+ type: "text",
647
+ text: promptContent,
648
+ },
649
+ ],
650
+ };
651
+ }
652
+ catch (error) {
653
+ console.error("Error using PowerPlatform prompt:", error);
654
+ return {
655
+ content: [
656
+ {
657
+ type: "text",
658
+ text: `Failed to use PowerPlatform prompt: ${error.message}`,
99
659
  },
100
660
  ],
101
661
  };
package/package.json CHANGED
@@ -1,47 +1,47 @@
1
- {
2
- "name": "powerplatform-mcp",
3
- "version": "0.2.0",
4
- "description": "PowerPlatform Model Context Protocol server",
5
- "main": "build/index.js",
6
- "bin": {
7
- "powerplatform-mcp": "./build/index.js"
8
- },
9
- "scripts": {
10
- "build": "tsc",
11
- "prepublishOnly": "npm run build"
12
- },
13
- "files": [
14
- "build",
15
- "README.md"
16
- ],
17
- "keywords": [
18
- "powerplatform",
19
- "mcp",
20
- "model-context-protocol",
21
- "dynamics",
22
- "dataverse"
23
- ],
24
- "author": "Michal Sobieraj",
25
- "license": "MIT",
26
- "type": "module",
27
- "publishConfig": {
28
- "access": "public"
29
- },
30
- "repository": {
31
- "type": "git",
32
- "url": "git+https://github.com/michsob/powerplatform-mcp.git"
33
- },
34
- "engines": {
35
- "node": ">=16.0.0"
36
- },
37
- "dependencies": {
38
- "@azure/msal-node": "^3.3.0",
39
- "@modelcontextprotocol/sdk": "^1.7.0",
40
- "axios": "^1.8.3",
41
- "zod": "^3.24.2"
42
- },
43
- "devDependencies": {
44
- "@types/node": "^22.13.10",
45
- "typescript": "^5.8.2"
46
- }
47
- }
1
+ {
2
+ "name": "powerplatform-mcp",
3
+ "version": "0.4.0",
4
+ "description": "PowerPlatform Model Context Protocol server",
5
+ "main": "build/index.js",
6
+ "bin": {
7
+ "powerplatform-mcp": "build/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "files": [
14
+ "build",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "powerplatform",
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "dynamics",
22
+ "dataverse"
23
+ ],
24
+ "author": "Michal Sobieraj",
25
+ "license": "MIT",
26
+ "type": "module",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/michsob/powerplatform-mcp.git"
33
+ },
34
+ "engines": {
35
+ "node": ">=16.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@azure/msal-node": "^3.3.0",
39
+ "@modelcontextprotocol/sdk": "^1.7.0",
40
+ "axios": "^1.8.3",
41
+ "zod": "^3.24.2"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.13.10",
45
+ "typescript": "^5.8.2"
46
+ }
47
+ }