PowerPlatform MCP

by michsob
Verified
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { PowerPlatformService, PowerPlatformConfig } from "./PowerPlatformService.js"; // Environment configuration // These values can be set in environment variables or loaded from a configuration file const POWERPLATFORM_CONFIG: PowerPlatformConfig = { organizationUrl: process.env.POWERPLATFORM_URL || "", clientId: process.env.POWERPLATFORM_CLIENT_ID || "", clientSecret: process.env.POWERPLATFORM_CLIENT_SECRET || "", tenantId: process.env.POWERPLATFORM_TENANT_ID || "", }; // Create server instance const server = new McpServer({ name: "powerplatform-mcp", version: "1.0.0", }); let powerPlatformService: PowerPlatformService | null = null; // Function to initialize PowerPlatformService on demand function getPowerPlatformService(): PowerPlatformService { if (!powerPlatformService) { // Check if configuration is complete const missingConfig: string[] = []; if (!POWERPLATFORM_CONFIG.organizationUrl) missingConfig.push("organizationUrl"); if (!POWERPLATFORM_CONFIG.clientId) missingConfig.push("clientId"); if (!POWERPLATFORM_CONFIG.clientSecret) missingConfig.push("clientSecret"); if (!POWERPLATFORM_CONFIG.tenantId) missingConfig.push("tenantId"); if (missingConfig.length > 0) { throw new Error(`Missing PowerPlatform configuration: ${missingConfig.join(", ")}. Set these in environment variables.`); } // Initialize service powerPlatformService = new PowerPlatformService(POWERPLATFORM_CONFIG); console.error("PowerPlatform service initialized"); } return powerPlatformService; } // Pre-defined PowerPlatform Prompts const powerPlatformPrompts = { // Entity exploration prompts ENTITY_OVERVIEW: (entityName: string) => `## Power Platform Entity: ${entityName}\n\n` + `This is an overview of the '${entityName}' entity in Microsoft Power Platform/Dataverse:\n\n` + `### Entity Details\n{{entity_details}}\n\n` + `### Attributes\n{{key_attributes}}\n\n` + `### Relationships\n{{relationships}}\n\n` + `You can query this entity using OData filters against the plural name.`, ATTRIBUTE_DETAILS: (entityName: string, attributeName: string) => `## Attribute: ${attributeName}\n\n` + `Details for the '${attributeName}' attribute of the '${entityName}' entity:\n\n` + `{{attribute_details}}\n\n` + `### Usage Notes\n` + `- Data Type: {{data_type}}\n` + `- Required: {{required}}\n` + `- Max Length: {{max_length}}`, // Query builder prompts QUERY_TEMPLATE: (entityNamePlural: string) => `## OData Query Template for ${entityNamePlural}\n\n` + `Use this template to build queries against the ${entityNamePlural} entity:\n\n` + `\`\`\`\n${entityNamePlural}?$select={{selected_fields}}&$filter={{filter_conditions}}&$orderby={{order_by}}&$top={{max_records}}\n\`\`\`\n\n` + `### Common Filter Examples\n` + `- Equals: \`name eq 'Contoso'\`\n` + `- Contains: \`contains(name, 'Contoso')\`\n` + `- Greater than date: \`createdon gt 2023-01-01T00:00:00Z\`\n` + `- Multiple conditions: \`name eq 'Contoso' and statecode eq 0\``, // Relationship exploration prompts RELATIONSHIP_MAP: (entityName: string) => `## Relationship Map for ${entityName}\n\n` + `This shows all relationships for the '${entityName}' entity:\n\n` + `### One-to-Many Relationships (${entityName} as Primary)\n{{one_to_many_primary}}\n\n` + `### One-to-Many Relationships (${entityName} as Related)\n{{one_to_many_related}}\n\n` + `### Many-to-Many Relationships\n{{many_to_many}}\n\n` }; // Register prompts with the server using the correct method signature // Entity Overview Prompt server.prompt( "entity-overview", "Get an overview of a Power Platform entity", { entityName: z.string().describe("The logical name of the entity") }, async (args) => { try { const service = getPowerPlatformService(); const entityName = args.entityName; // Get entity metadata and key attributes const [metadata, attributes] = await Promise.all([ service.getEntityMetadata(entityName), service.getEntityAttributes(entityName) ]); // Format entity details const entityDetails = `- Display Name: ${metadata.DisplayName?.UserLocalizedLabel?.Label || entityName}\n` + `- Schema Name: ${metadata.SchemaName}\n` + `- Description: ${metadata.Description?.UserLocalizedLabel?.Label || 'No description'}\n` + `- Primary Key: ${metadata.PrimaryIdAttribute}\n` + `- Primary Name: ${metadata.PrimaryNameAttribute}`; // Get key attributes const keyAttributes = attributes.value .map((attr: any) => { const attrType = attr["@odata.type"] || attr.odata?.type || "Unknown type"; return `- ${attr.LogicalName}: ${attrType}`; }) .join('\n'); // Get relationships summary const relationships = await service.getEntityRelationships(entityName); const oneToManyCount = relationships.oneToMany.value.length; const manyToManyCount = relationships.manyToMany.value.length; const relationshipsSummary = `- One-to-Many Relationships: ${oneToManyCount}\n` + `- Many-to-Many Relationships: ${manyToManyCount}`; let promptContent = powerPlatformPrompts.ENTITY_OVERVIEW(entityName); promptContent = promptContent .replace('{{entity_details}}', entityDetails) .replace('{{key_attributes}}', keyAttributes) .replace('{{relationships}}', relationshipsSummary); return { messages: [ { role: "assistant", content: { type: "text", text: promptContent } } ] }; } catch (error: any) { console.error(`Error handling entity-overview prompt:`, error); return { messages: [ { role: "assistant", content: { type: "text", text: `Error: ${error.message}` } } ] }; } } ); // Attribute Details Prompt server.prompt( "attribute-details", "Get detailed information about a specific entity attribute/field", { entityName: z.string().describe("The logical name of the entity"), attributeName: z.string().describe("The logical name of the attribute"), }, async (args) => { try { const service = getPowerPlatformService(); const { entityName, attributeName } = args; // Get attribute details const attribute = await service.getEntityAttribute(entityName, attributeName); // Format attribute details const attrDetails = `- Display Name: ${attribute.DisplayName?.UserLocalizedLabel?.Label || attributeName}\n` + `- Description: ${attribute.Description?.UserLocalizedLabel?.Label || 'No description'}\n` + `- Type: ${attribute.AttributeType}\n` + `- Format: ${attribute.Format || 'N/A'}\n` + `- Is Required: ${attribute.RequiredLevel?.Value || 'No'}\n` + `- Is Searchable: ${attribute.IsValidForAdvancedFind || false}`; let promptContent = powerPlatformPrompts.ATTRIBUTE_DETAILS(entityName, attributeName); promptContent = promptContent .replace('{{attribute_details}}', attrDetails) .replace('{{data_type}}', attribute.AttributeType) .replace('{{required}}', attribute.RequiredLevel?.Value || 'No') .replace('{{max_length}}', attribute.MaxLength || 'N/A'); return { messages: [ { role: "assistant", content: { type: "text", text: promptContent } } ] }; } catch (error: any) { console.error(`Error handling attribute-details prompt:`, error); return { messages: [ { role: "assistant", content: { type: "text", text: `Error: ${error.message}` } } ] }; } } ); // Query Template Prompt server.prompt( "query-template", "Get a template for querying a Power Platform entity", { entityName: z.string().describe("The logical name of the entity"), }, async (args) => { try { const service = getPowerPlatformService(); const entityName = args.entityName; // Get entity metadata to determine plural name const metadata = await service.getEntityMetadata(entityName); const entityNamePlural = metadata.EntitySetName; // Get a few important fields for the select example const attributes = await service.getEntityAttributes(entityName); const selectFields = attributes.value .filter((attr: any) => attr.IsValidForRead === true && !attr.AttributeOf) .slice(0, 5) // Just take first 5 for example .map((attr: any) => attr.LogicalName) .join(','); let promptContent = powerPlatformPrompts.QUERY_TEMPLATE(entityNamePlural); promptContent = promptContent .replace('{{selected_fields}}', selectFields) .replace('{{filter_conditions}}', `${metadata.PrimaryNameAttribute} eq 'Example'`) .replace('{{order_by}}', `${metadata.PrimaryNameAttribute} asc`) .replace('{{max_records}}', '50'); return { messages: [ { role: "assistant", content: { type: "text", text: promptContent } } ] }; } catch (error: any) { console.error(`Error handling query-template prompt:`, error); return { messages: [ { role: "assistant", content: { type: "text", text: `Error: ${error.message}` } } ] }; } } ); // Relationship Map Prompt server.prompt( "relationship-map", "Get a list of relationships for a Power Platform entity", { entityName: z.string().describe("The logical name of the entity"), }, async (args) => { try { const service = getPowerPlatformService(); const entityName = args.entityName; // Get relationships const relationships = await service.getEntityRelationships(entityName); // Format one-to-many relationships where this entity is primary const oneToManyPrimary = relationships.oneToMany.value .filter((rel: any) => rel.ReferencingEntity !== entityName) .map((rel: any) => `- ${rel.SchemaName}: ${entityName} (1) → ${rel.ReferencingEntity} (N)`) .join('\n'); // Format one-to-many relationships where this entity is related const oneToManyRelated = relationships.oneToMany.value .filter((rel: any) => rel.ReferencingEntity === entityName) .map((rel: any) => `- ${rel.SchemaName}: ${rel.ReferencedEntity} (1) → ${entityName} (N)`) .join('\n'); // Format many-to-many relationships const manyToMany = relationships.manyToMany.value .map((rel: any) => { const otherEntity = rel.Entity1LogicalName === entityName ? rel.Entity2LogicalName : rel.Entity1LogicalName; return `- ${rel.SchemaName}: ${entityName} (N) ↔ ${otherEntity} (N)`; }) .join('\n'); let promptContent = powerPlatformPrompts.RELATIONSHIP_MAP(entityName); promptContent = promptContent .replace('{{one_to_many_primary}}', oneToManyPrimary || 'None found') .replace('{{one_to_many_related}}', oneToManyRelated || 'None found') .replace('{{many_to_many}}', manyToMany || 'None found'); return { messages: [ { role: "assistant", content: { type: "text", text: promptContent } } ] }; } catch (error: any) { console.error(`Error handling relationship-map prompt:`, error); return { messages: [ { role: "assistant", content: { type: "text", text: `Error: ${error.message}` } } ] }; } } ); // PowerPlatform entity metadata server.tool( "get-entity-metadata", "Get metadata about a PowerPlatform entity", { entityName: z.string().describe("The logical name of the entity"), }, async ({ entityName }) => { try { // Get or initialize PowerPlatformService const service = getPowerPlatformService(); const metadata = await service.getEntityMetadata(entityName); // Format the metadata as a string for text display const metadataStr = JSON.stringify(metadata, null, 2); return { content: [ { type: "text", text: `Entity metadata for '${entityName}':\n\n${metadataStr}`, }, ], }; } catch (error: any) { console.error("Error getting entity metadata:", error); return { content: [ { type: "text", text: `Failed to get entity metadata: ${error.message}`, }, ], }; } } ); // PowerPlatform entity attributes server.tool( "get-entity-attributes", "Get attributes/fields of a PowerPlatform entity", { entityName: z.string().describe("The logical name of the entity"), }, async ({ entityName }) => { try { // Get or initialize PowerPlatformService const service = getPowerPlatformService(); const attributes = await service.getEntityAttributes(entityName); // Format the attributes as a string for text display const attributesStr = JSON.stringify(attributes, null, 2); return { content: [ { type: "text", text: `Attributes for entity '${entityName}':\n\n${attributesStr}`, }, ], }; } catch (error: any) { console.error("Error getting entity attributes:", error); return { content: [ { type: "text", text: `Failed to get entity attributes: ${error.message}`, }, ], }; } } ); // PowerPlatform specific entity attribute server.tool( "get-entity-attribute", "Get a specific attribute/field of a PowerPlatform entity", { entityName: z.string().describe("The logical name of the entity"), attributeName: z.string().describe("The logical name of the attribute") }, async ({ entityName, attributeName }) => { try { // Get or initialize PowerPlatformService const service = getPowerPlatformService(); const attribute = await service.getEntityAttribute(entityName, attributeName); // Format the attribute as a string for text display const attributeStr = JSON.stringify(attribute, null, 2); return { content: [ { type: "text", text: `Attribute '${attributeName}' for entity '${entityName}':\n\n${attributeStr}`, }, ], }; } catch (error: any) { console.error("Error getting entity attribute:", error); return { content: [ { type: "text", text: `Failed to get entity attribute: ${error.message}`, }, ], }; } } ); // PowerPlatform entity relationships server.tool( "get-entity-relationships", "Get relationships (one-to-many and many-to-many) for a PowerPlatform entity", { entityName: z.string().describe("The logical name of the entity"), }, async ({ entityName }) => { try { // Get or initialize PowerPlatformService const service = getPowerPlatformService(); const relationships = await service.getEntityRelationships(entityName); // Format the relationships as a string for text display const relationshipsStr = JSON.stringify(relationships, null, 2); return { content: [ { type: "text", text: `Relationships for entity '${entityName}':\n\n${relationshipsStr}`, }, ], }; } catch (error: any) { console.error("Error getting entity relationships:", error); return { content: [ { type: "text", text: `Failed to get entity relationships: ${error.message}`, }, ], }; } } ); // PowerPlatform global option set server.tool( "get-global-option-set", "Get a global option set definition by name", { optionSetName: z.string().describe("The name of the global option set"), }, async ({ optionSetName }) => { try { // Get or initialize PowerPlatformService const service = getPowerPlatformService(); const optionSet = await service.getGlobalOptionSet(optionSetName); // Format the option set as a string for text display const optionSetStr = JSON.stringify(optionSet, null, 2); return { content: [ { type: "text", text: `Global option set '${optionSetName}':\n\n${optionSetStr}`, }, ], }; } catch (error: any) { console.error("Error getting global option set:", error); return { content: [ { type: "text", text: `Failed to get global option set: ${error.message}`, }, ], }; } } ); // PowerPlatform record by ID server.tool( "get-record", "Get a specific record by entity name (plural) and ID", { entityNamePlural: z.string().describe("The plural name of the entity (e.g., 'accounts', 'contacts')"), recordId: z.string().describe("The GUID of the record"), }, async ({ entityNamePlural, recordId }) => { try { // Get or initialize PowerPlatformService const service = getPowerPlatformService(); const record = await service.getRecord(entityNamePlural, recordId); // Format the record as a string for text display const recordStr = JSON.stringify(record, null, 2); return { content: [ { type: "text", text: `Record from '${entityNamePlural}' with ID '${recordId}':\n\n${recordStr}`, }, ], }; } catch (error: any) { console.error("Error getting record:", error); return { content: [ { type: "text", text: `Failed to get record: ${error.message}`, }, ], }; } } ); // PowerPlatform query records with filter server.tool( "query-records", "Query records using an OData filter expression", { entityNamePlural: z.string().describe("The plural name of the entity (e.g., 'accounts', 'contacts')"), filter: z.string().describe("OData filter expression (e.g., \"name eq 'test'\" or \"createdon gt 2023-01-01\")"), maxRecords: z.number().optional().describe("Maximum number of records to retrieve (default: 50)"), }, async ({ entityNamePlural, filter, maxRecords }) => { try { // Get or initialize PowerPlatformService const service = getPowerPlatformService(); const records = await service.queryRecords(entityNamePlural, filter, maxRecords || 50); // Format the records as a string for text display const recordsStr = JSON.stringify(records, null, 2); const recordCount = records.value?.length || 0; return { content: [ { type: "text", text: `Retrieved ${recordCount} records from '${entityNamePlural}' with filter '${filter}':\n\n${recordsStr}`, }, ], }; } catch (error: any) { console.error("Error querying records:", error); return { content: [ { type: "text", text: `Failed to query records: ${error.message}`, }, ], }; } } ); // PowerPlatform MCP Prompts server.tool( "use-powerplatform-prompt", "Use a predefined prompt template for PowerPlatform entities", { promptType: z.enum([ "ENTITY_OVERVIEW", "ATTRIBUTE_DETAILS", "QUERY_TEMPLATE", "RELATIONSHIP_MAP" ]).describe("The type of prompt template to use"), entityName: z.string().describe("The logical name of the entity"), attributeName: z.string().optional().describe("The logical name of the attribute (required for ATTRIBUTE_DETAILS prompt)"), }, async ({ promptType, entityName, attributeName }) => { try { // Get or initialize PowerPlatformService const service = getPowerPlatformService(); let promptContent = ""; let replacements: Record<string, string> = {}; switch (promptType) { case "ENTITY_OVERVIEW": { // Get entity metadata and key attributes const [metadata, attributes] = await Promise.all([ service.getEntityMetadata(entityName), service.getEntityAttributes(entityName) ]); // Format entity details const entityDetails = `- Display Name: ${metadata.DisplayName?.UserLocalizedLabel?.Label || entityName}\n` + `- Schema Name: ${metadata.SchemaName}\n` + `- Description: ${metadata.Description?.UserLocalizedLabel?.Label || 'No description'}\n` + `- Primary Key: ${metadata.PrimaryIdAttribute}\n` + `- Primary Name: ${metadata.PrimaryNameAttribute}`; // Get key attributes const keyAttributes = attributes.value //.slice(0, 10) // Limit to first 10 important attributes .map((attr: any) => { const attrType = attr["@odata.type"] || attr.odata?.type || "Unknown type"; return `- ${attr.LogicalName}: ${attrType}`; }) .join('\n'); // Get relationships summary const relationships = await service.getEntityRelationships(entityName); const oneToManyCount = relationships.oneToMany.value.length; const manyToManyCount = relationships.manyToMany.value.length; const relationshipsSummary = `- One-to-Many Relationships: ${oneToManyCount}\n` + `- Many-to-Many Relationships: ${manyToManyCount}`; promptContent = powerPlatformPrompts.ENTITY_OVERVIEW(entityName); replacements = { '{{entity_details}}': entityDetails, '{{key_attributes}}': keyAttributes, '{{relationships}}': relationshipsSummary }; break; } case "ATTRIBUTE_DETAILS": { if (!attributeName) { throw new Error("attributeName is required for ATTRIBUTE_DETAILS prompt"); } // Get attribute details const attribute = await service.getEntityAttribute(entityName, attributeName); // Format attribute details const attrDetails = `- Display Name: ${attribute.DisplayName?.UserLocalizedLabel?.Label || attributeName}\n` + `- Description: ${attribute.Description?.UserLocalizedLabel?.Label || 'No description'}\n` + `- Type: ${attribute.AttributeType}\n` + `- Format: ${attribute.Format || 'N/A'}\n` + `- Is Required: ${attribute.RequiredLevel?.Value || 'No'}\n` + `- Is Searchable: ${attribute.IsValidForAdvancedFind || false}`; promptContent = powerPlatformPrompts.ATTRIBUTE_DETAILS(entityName, attributeName); replacements = { '{{attribute_details}}': attrDetails, '{{data_type}}': attribute.AttributeType, '{{required}}': attribute.RequiredLevel?.Value || 'No', '{{max_length}}': attribute.MaxLength || 'N/A' }; break; } case "QUERY_TEMPLATE": { // Get entity metadata to determine plural name const metadata = await service.getEntityMetadata(entityName); const entityNamePlural = metadata.EntitySetName; // Get a few important fields for the select example const attributes = await service.getEntityAttributes(entityName); const selectFields = attributes.value .slice(0, 5) // Just take first 5 for example .map((attr: any) => attr.LogicalName) .join(','); promptContent = powerPlatformPrompts.QUERY_TEMPLATE(entityNamePlural); replacements = { '{{selected_fields}}': selectFields, '{{filter_conditions}}': `${metadata.PrimaryNameAttribute} eq 'Example'`, '{{order_by}}': `${metadata.PrimaryNameAttribute} asc`, '{{max_records}}': '50' }; break; } case "RELATIONSHIP_MAP": { // Get relationships const relationships = await service.getEntityRelationships(entityName); // Format one-to-many relationships where this entity is primary const oneToManyPrimary = relationships.oneToMany.value .filter((rel: any) => rel.ReferencingEntity !== entityName) //.slice(0, 10) // Limit to 10 for readability .map((rel: any) => `- ${rel.SchemaName}: ${entityName} (1) → ${rel.ReferencingEntity} (N)`) .join('\n'); // Format one-to-many relationships where this entity is related const oneToManyRelated = relationships.oneToMany.value .filter((rel: any) => rel.ReferencingEntity === entityName) //.slice(0, 10) // Limit to 10 for readability .map((rel: any) => `- ${rel.SchemaName}: ${rel.ReferencedEntity} (1) → ${entityName} (N)`) .join('\n'); // Format many-to-many relationships const manyToMany = relationships.manyToMany.value //.slice(0, 10) // Limit to 10 for readability .map((rel: any) => { const otherEntity = rel.Entity1LogicalName === entityName ? rel.Entity2LogicalName : rel.Entity1LogicalName; return `- ${rel.SchemaName}: ${entityName} (N) ↔ ${otherEntity} (N)`; }) .join('\n'); promptContent = powerPlatformPrompts.RELATIONSHIP_MAP(entityName); replacements = { '{{one_to_many_primary}}': oneToManyPrimary || 'None found', '{{one_to_many_related}}': oneToManyRelated || 'None found', '{{many_to_many}}': manyToMany || 'None found' }; break; } } // Replace all placeholders in the template for (const [placeholder, value] of Object.entries(replacements)) { promptContent = promptContent.replace(placeholder, value); } return { content: [ { type: "text", text: promptContent, }, ], }; } catch (error: any) { console.error("Error using PowerPlatform prompt:", error); return { content: [ { type: "text", text: `Failed to use PowerPlatform prompt: ${error.message}`, }, ], }; } } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Initializing PowerPlatform MCP Server..."); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });