# Directory Structure
```
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── index.ts
│ └── PowerPlatformService.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
#Build output
build
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# PowerPlatform MCP Server
A Model Context Protocol (MCP) server that provides intelligent access to PowerPlatform/Dataverse entities and records. This tool offers context-aware assistance, entity exploration and metadata access.
Key features:
- Rich entity metadata exploration with formatted, context-aware prompts
- Advanced OData query support with intelligent filtering
- Comprehensive relationship mapping and visualization
- AI-assisted query building and data modeling through AI agent
- Full access to entity attributes, relationships, and global option sets
## Installation
You can install and run this tool in two ways:
### Option 1: Install globally
```bash
npm install -g powerplatform-mcp
```
Then run it:
```bash
powerplatform-mcp
```
### Option 2: Run directly with npx
Run without installing:
```bash
npx powerplatform-mcp
```
## Configuration
Before running, set the following environment variables:
```bash
# PowerPlatform/Dataverse connection details
POWERPLATFORM_URL=https://yourenvironment.crm.dynamics.com
POWERPLATFORM_CLIENT_ID=your-azure-app-client-id
POWERPLATFORM_CLIENT_SECRET=your-azure-app-client-secret
POWERPLATFORM_TENANT_ID=your-azure-tenant-id
```
## Usage
This is an MCP server designed to work with MCP-compatible clients like Cursor, Claude App and GitHub Copilot. Once running, it will expose tools for retrieving PowerPlatform entity metadata and records.
### Available Tools
- `get-entity-metadata`: Get metadata about a PowerPlatform entity
- `get-entity-attributes`: Get attributes/fields of a PowerPlatform entity
- `get-entity-attribute`: Get a specific attribute/field of a PowerPlatform entity
- `get-entity-relationships`: Get relationships for a PowerPlatform entity
- `get-global-option-set`: Get a global option set definition
- `get-record`: Get a specific record by entity name and ID
- `query-records`: Query records using an OData filter expression
- `use-powerplatform-prompt`: Use pre-defined prompt templates for PowerPlatform entities
## MCP Prompts
The server includes a prompts feature that provides formatted, context-rich information about PowerPlatform entities.
### Available Prompt Types
The `use-powerplatform-prompt` tool supports the following prompt types:
1. **ENTITY_OVERVIEW**: Comprehensive overview of an entity
2. **ATTRIBUTE_DETAILS**: Detailed information about a specific entity attribute
3. **QUERY_TEMPLATE**: OData query template for an entity with example filters
4. **RELATIONSHIP_MAP**: Visual map of entity relationships
### Examples
#### Entity Overview Prompt
```javascript
// Example client code
await mcpClient.invoke("use-powerplatform-prompt", {
promptType: "ENTITY_OVERVIEW",
entityName: "account"
});
```
**Output:**
```
## Power Platform Entity: account
This is an overview of the 'account' entity in Microsoft Power Platform/Dataverse:
### Entity Details
- Display Name: Account
- Schema Name: Account
- Description: Business that represents a customer or potential customer
- Primary Key: accountid
- Primary Name: name
### Key Attributes
- name: String (Account Name)
- emailaddress1: String (Email)
- telephone1: String (Main Phone)
- address1_city: String (City)
- statecode: Status (Status)
- accountnumber: String (Account Number)
- industrycode: OptionSetValue (Industry)
- revenue: Money (Annual Revenue)
- ownerid: Owner (Owner)
- createdon: DateTime (Created On)
### Relationships
- One-to-Many Relationships: 42
- Many-to-Many Relationships: 8
You can query this entity using OData filters against the plural name.
```
#### Attribute Details Prompt
```javascript
await mcpClient.invoke("use-powerplatform-prompt", {
promptType: "ATTRIBUTE_DETAILS",
entityName: "account",
attributeName: "revenue"
});
```
**Output:**
```
## Attribute: revenue
Details for the 'revenue' attribute of the 'account' entity:
- Display Name: Annual Revenue
- Description: Annual revenue for the account
- Type: Money
- Format: Currency
- Is Required: No
- Is Searchable: true
### Usage Notes
- Data Type: Money
- Required: No
- Max Length: N/A
```
#### Query Template Prompt
```javascript
await mcpClient.invoke("use-powerplatform-prompt", {
promptType: "QUERY_TEMPLATE",
entityName: "account"
});
```
**Output:**
```
## OData Query Template for accounts
Use this template to build queries against the accounts entity:
accounts?$select=name,emailaddress1,telephone1, address1_city,statecode&$filter=name eq 'Example'&$orderby=name asc&$top=50
```
### Common Filter Examples
- Equals: `name eq 'Contoso'`
- Contains: `contains(name, 'Contoso')`
- Greater than date: `createdon gt 2023-01-01T00:00:00Z`
- Multiple conditions: `name eq 'Contoso' and statecode eq 0`
```
#### Relationship Map Prompt
```javascript
await mcpClient.invoke("use-powerplatform-prompt", {
promptType: "RELATIONSHIP_MAP",
entityName: "contact"
});
```
**Output:**
```
## Relationship Map for contact
This shows all relationships for the 'contact' entity:
### One-to-Many Relationships (contact as Primary)
- contact_activity_parties: contact (1) → activityparty (N)
- contact_connections1: contact (1) → connection (N)
- contact_connections2: contact (1) → connection (N)
- contact_customer_contacts: contact (1) → contact (N)
- contact_master_contact: contact (1) → contact (N)
### One-to-Many Relationships (contact as Related)
- account_primary_contact: account (1) → contact (N)
- customer_contacts: customer (1) → contact (N)
- system_user_contacts: systemuser (1) → contact (N)
### Many-to-Many Relationships
- contactleads_association: contact (N) ↔ lead (N)
- contactopportunities_association: contact (N) ↔ opportunity (N)
- contactquotes_association: contact (N) ↔ quote (N)
```
## Prompt Examples
Here are some examples of how you can interact with assistant using the PowerPlatform MCP prompts:
### 1. Entity Understanding
You can ask assistant about entities in natural language:
```
User: "Tell me about the Account entity in our PowerPlatform environment"
Response:
[uses ENTITY_OVERVIEW prompt internally]
Here's what I found about the Account entity:
- It's a core business entity representing customers and potential customers
- The primary identifier is 'accountid' and the primary name field is 'name'
- Key fields include:
* name (Account Name)
* emailaddress1 (Email)
* telephone1 (Main Phone)
* revenue (Annual Revenue)
- It has 42 one-to-many relationships and 8 many-to-many relationships
```
### 2. Building Queries
You can ask for help constructing queries:
```
User: "Help me write a query to find active accounts with revenue over $1M in California"
Response:
[uses QUERY_TEMPLATE prompt internally]
Here's the OData query you need:
accounts?$select=name,revenue,statecode,address1_stateorprovince
&$filter=statecode eq 0
and revenue gt 1000000
and address1_stateorprovince eq 'California'
&$orderby=revenue desc
This query:
1. Selects relevant fields
2. Filters for active accounts (statecode eq 0)
3. Checks revenue > $1M
4. Filters for California location
5. Orders by revenue highest to lowest
```
### 3. Understanding Relationships
You can explore entity relationships naturally:
```
User: "How are Accounts and Contacts related in our system?"
Response:
[uses RELATIONSHIP_MAP prompt for both entities internally]
Here are the key relationships between Accounts and Contacts:
1. Primary Contact (One-to-Many):
- account_primary_contact: Account (1) → Contact (N)
- An account can have many contacts, but one designated as primary
2. Customer Relationship:
- contact_customer_accounts: Contact (N) ← Account (1)
- Contacts can be associated with customer accounts
3. Additional Relationships:
- accountleads_association: For lead tracking
- account_connections: For relationship management
```
### 4. Field/Attribute Investigation
You can ask about specific fields:
```
User: "What kind of field is the revenue field on Account?"
Response:
[uses ATTRIBUTE_DETAILS prompt internally]
The revenue field on Account is:
- Data Type: Money
- Format: Currency
- Not required
- Searchable: Yes
- Used for: Tracking annual revenue for the account
- Can be used in advanced find and filtered views
```
### 5. Complex Data Modeling
You can get help with data modeling decisions:
```
User: "I need to design a solution for tracking product reviews. Which entities should I relate to?"
Response:
[uses multiple ENTITY_OVERVIEW and RELATIONSHIP_MAP prompts internally]
Based on the existing data model, here's a suggested approach:
1. Product (existing entity):
- Already has relationships with:
* Price lists
* Product categories
* Inventory units
2. New Review entity should relate to:
- Product (N:1) - Each review is for one product
- Contact (N:1) - Reviews are written by contacts
- Account (N:1) - Optional link to company if B2B review
Here's a sample lookup field structure...
[continues with detailed implementation guidance]
```
### 6. Debugging Assistance
You can get help troubleshooting:
```
User: "Why am I getting 'invalid property' when querying account.customertypecode?"
Response:
[uses ATTRIBUTE_DETAILS prompt internally]
I see the issue. The field 'customertypecode' is actually named 'customertype' in your environment. Here's the correct field information:
- Logical Name: customertype
- Type: OptionSetValue
- Valid Values:
* 1 = Competitor
* 2 = Consultant
* 3 = Customer
* 4 = Investor
* 5 = Partner
* 6 = Influencer
Try updating your query to use 'customertype' instead of 'customertypecode'.
```
These examples show how AI assistant 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.
## License
MIT
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "powerplatform-mcp",
"version": "0.4.5",
"description": "PowerPlatform Model Context Protocol server",
"main": "build/index.js",
"bin": {
"powerplatform-mcp": "build/index.js"
},
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"files": [
"build",
"README.md"
],
"keywords": [
"powerplatform",
"mcp",
"model-context-protocol",
"dynamics",
"dataverse"
],
"author": "Michal Sobieraj",
"license": "MIT",
"type": "module",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/michsob/powerplatform-mcp.git"
},
"engines": {
"node": ">=16.0.0"
},
"dependencies": {
"@azure/msal-node": "^3.3.0",
"@modelcontextprotocol/sdk": "^1.7.0",
"axios": "^1.8.3",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^22.13.10",
"typescript": "^5.8.2"
}
}
```
--------------------------------------------------------------------------------
/src/PowerPlatformService.ts:
--------------------------------------------------------------------------------
```typescript
import { ConfidentialClientApplication } from '@azure/msal-node';
import axios from 'axios';
export interface PowerPlatformConfig {
organizationUrl: string;
clientId: string;
clientSecret: string;
tenantId: string;
}
// Interface for API responses with value collections
export interface ApiCollectionResponse<T> {
value: T[];
[key: string]: any; // For any additional properties
}
export class PowerPlatformService {
private config: PowerPlatformConfig;
private msalClient: ConfidentialClientApplication;
private accessToken: string | null = null;
private tokenExpirationTime: number = 0;
constructor(config: PowerPlatformConfig) {
this.config = config;
// Initialize MSAL client
this.msalClient = new ConfidentialClientApplication({
auth: {
clientId: this.config.clientId,
clientSecret: this.config.clientSecret,
authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
}
});
}
/**
* Get an access token for the PowerPlatform API
*/
private async getAccessToken(): Promise<string> {
const currentTime = Date.now();
// If we have a token that isn't expired, return it
if (this.accessToken && this.tokenExpirationTime > currentTime) {
return this.accessToken;
}
try {
// Get a new token
const result = await this.msalClient.acquireTokenByClientCredential({
scopes: [`${this.config.organizationUrl}/.default`],
});
if (!result || !result.accessToken) {
throw new Error('Failed to acquire access token');
}
this.accessToken = result.accessToken;
// Set expiration time (subtract 5 minutes to refresh early)
if (result.expiresOn) {
this.tokenExpirationTime = result.expiresOn.getTime() - (5 * 60 * 1000);
}
return this.accessToken;
} catch (error) {
console.error('Error acquiring access token:', error);
throw new Error('Authentication failed');
}
}
/**
* Make an authenticated request to the PowerPlatform API
*/
private async makeRequest<T>(endpoint: string): Promise<T> {
try {
const token = await this.getAccessToken();
const response = await axios({
method: 'GET',
url: `${this.config.organizationUrl}/${endpoint}`,
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0'
}
});
return response.data as T;
} catch (error) {
console.error('PowerPlatform API request failed:', error);
throw new Error(`PowerPlatform API request failed: ${error}`);
}
}
/**
* Get metadata about an entity
* @param entityName The logical name of the entity
*/
async getEntityMetadata(entityName: string): Promise<any> {
const response = await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')`);
// Remove Privileges property if it exists
if (response && typeof response === 'object' && 'Privileges' in response) {
delete response.Privileges;
}
return response;
}
/**
* Get metadata about entity attributes/fields
* @param entityName The logical name of the entity
*/
async getEntityAttributes(entityName: string): Promise<ApiCollectionResponse<any>> {
const selectProperties = [
'LogicalName',
].join(',');
// Make the request to get attributes
const response = await this.makeRequest<ApiCollectionResponse<any>>(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes?$select=${selectProperties}&$filter=AttributeType ne 'Virtual'`);
if (response && response.value) {
// First pass: Filter out attributes that end with 'yominame'
response.value = response.value.filter((attribute: any) => {
const logicalName = attribute.LogicalName || '';
return !logicalName.endsWith('yominame');
});
// Filter out attributes that end with 'name' if there is another attribute with the same name without the 'name' suffix
const baseNames = new Set<string>();
const namesAttributes = new Map<string, any>();
for (const attribute of response.value) {
const logicalName = attribute.LogicalName || '';
if (logicalName.endsWith('name') && logicalName.length > 4) {
const baseName = logicalName.slice(0, -4); // Remove 'name' suffix
namesAttributes.set(baseName, attribute);
} else {
// This is a potential base attribute
baseNames.add(logicalName);
}
}
// Find attributes to remove that match the pattern
const attributesToRemove = new Set<any>();
for (const [baseName, nameAttribute] of namesAttributes.entries()) {
if (baseNames.has(baseName)) {
attributesToRemove.add(nameAttribute);
}
}
response.value = response.value.filter(attribute => !attributesToRemove.has(attribute));
}
return response;
}
/**
* Get metadata about a specific entity attribute/field
* @param entityName The logical name of the entity
* @param attributeName The logical name of the attribute
*/
async getEntityAttribute(entityName: string, attributeName: string): Promise<any> {
return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes(LogicalName='${attributeName}')`);
}
/**
* Get one-to-many relationships for an entity
* @param entityName The logical name of the entity
*/
async getEntityOneToManyRelationships(entityName: string): Promise<ApiCollectionResponse<any>> {
const selectProperties = [
'SchemaName',
'RelationshipType',
'ReferencedAttribute',
'ReferencedEntity',
'ReferencingAttribute',
'ReferencingEntity',
'ReferencedEntityNavigationPropertyName',
'ReferencingEntityNavigationPropertyName'
].join(',');
// Only filter by ReferencingAttribute in the OData query since startswith isn't supported
const response = await this.makeRequest<ApiCollectionResponse<any>>(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/OneToManyRelationships?$select=${selectProperties}&$filter=ReferencingAttribute ne 'regardingobjectid'`);
// Filter the response to exclude relationships with ReferencingEntity starting with 'msdyn_' or 'adx_'
if (response && response.value) {
response.value = response.value.filter((relationship: any) => {
const referencingEntity = relationship.ReferencingEntity || '';
return !(referencingEntity.startsWith('msdyn_') || referencingEntity.startsWith('adx_'));
});
}
return response;
}
/**
* Get many-to-many relationships for an entity
* @param entityName The logical name of the entity
*/
async getEntityManyToManyRelationships(entityName: string): Promise<ApiCollectionResponse<any>> {
const selectProperties = [
'SchemaName',
'RelationshipType',
'Entity1LogicalName',
'Entity2LogicalName',
'Entity1IntersectAttribute',
'Entity2IntersectAttribute',
'Entity1NavigationPropertyName',
'Entity2NavigationPropertyName'
].join(',');
return this.makeRequest<ApiCollectionResponse<any>>(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/ManyToManyRelationships?$select=${selectProperties}`);
}
/**
* Get all relationships (one-to-many and many-to-many) for an entity
* @param entityName The logical name of the entity
*/
async getEntityRelationships(entityName: string): Promise<{oneToMany: ApiCollectionResponse<any>, manyToMany: ApiCollectionResponse<any>}> {
const [oneToMany, manyToMany] = await Promise.all([
this.getEntityOneToManyRelationships(entityName),
this.getEntityManyToManyRelationships(entityName)
]);
return {
oneToMany,
manyToMany
};
}
/**
* Get a global option set definition by name
* @param optionSetName The name of the global option set
* @returns The global option set definition
*/
async getGlobalOptionSet(optionSetName: string): Promise<any> {
return this.makeRequest(`api/data/v9.2/GlobalOptionSetDefinitions(Name='${optionSetName}')`);
}
/**
* Get a specific record by entity name (plural) and ID
* @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
* @param recordId The GUID of the record
* @returns The record data
*/
async getRecord(entityNamePlural: string, recordId: string): Promise<any> {
return this.makeRequest(`api/data/v9.2/${entityNamePlural}(${recordId})`);
}
/**
* Query records using entity name (plural) and a filter expression
* @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
* @param filter OData filter expression (e.g., "name eq 'test'")
* @param maxRecords Maximum number of records to retrieve (default: 50)
* @returns Filtered list of records
*/
async queryRecords(entityNamePlural: string, filter: string, maxRecords: number = 50): Promise<ApiCollectionResponse<any>> {
return this.makeRequest<ApiCollectionResponse<any>>(`api/data/v9.2/${entityNamePlural}?$filter=${encodeURIComponent(filter)}&$top=${maxRecords}`);
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/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);
});
```