#
tokens: 15504/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── index.ts
│   └── PowerPlatformService.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | #Build output
 87 | build
 88 | 
 89 | # Next.js build output
 90 | .next
 91 | out
 92 | 
 93 | # Nuxt.js build / generate output
 94 | .nuxt
 95 | dist
 96 | 
 97 | 
 98 | # Gatsby files
 99 | .cache/
100 | # Comment in the public line in if your project uses Gatsby and not Next.js
101 | # https://nextjs.org/blog/next-9-1#public-directory-support
102 | # public
103 | 
104 | # vuepress build output
105 | .vuepress/dist
106 | 
107 | # vuepress v2.x temp and cache directory
108 | .temp
109 | .cache
110 | 
111 | # vitepress build output
112 | **/.vitepress/dist
113 | 
114 | # vitepress cache directory
115 | **/.vitepress/cache
116 | 
117 | # Docusaurus cache and generated files
118 | .docusaurus
119 | 
120 | # Serverless directories
121 | .serverless/
122 | 
123 | # FuseBox cache
124 | .fusebox/
125 | 
126 | # DynamoDB Local files
127 | .dynamodb/
128 | 
129 | # TernJS port file
130 | .tern-port
131 | 
132 | # Stores VSCode versions used for testing VSCode extensions
133 | .vscode-test
134 | 
135 | # yarn v2
136 | .yarn/cache
137 | .yarn/unplugged
138 | .yarn/build-state.yml
139 | .yarn/install-state.gz
140 | .pnp.*
141 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # PowerPlatform MCP Server
  2 | 
  3 | 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.
  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 | ## Installation
 13 | 
 14 | You can install and run this tool in two ways:
 15 | 
 16 | ### Option 1: Install globally
 17 | 
 18 | ```bash
 19 | npm install -g powerplatform-mcp
 20 | ```
 21 | 
 22 | Then run it:
 23 | 
 24 | ```bash
 25 | powerplatform-mcp
 26 | ```
 27 | 
 28 | ### Option 2: Run directly with npx
 29 | 
 30 | Run without installing:
 31 | 
 32 | ```bash
 33 | npx powerplatform-mcp
 34 | ```
 35 | 
 36 | ## Configuration
 37 | 
 38 | Before running, set the following environment variables:
 39 | 
 40 | ```bash
 41 | # PowerPlatform/Dataverse connection details
 42 | POWERPLATFORM_URL=https://yourenvironment.crm.dynamics.com
 43 | POWERPLATFORM_CLIENT_ID=your-azure-app-client-id
 44 | POWERPLATFORM_CLIENT_SECRET=your-azure-app-client-secret
 45 | POWERPLATFORM_TENANT_ID=your-azure-tenant-id
 46 | ```
 47 | 
 48 | ## Usage
 49 | 
 50 | 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.
 51 | 
 52 | ### Available Tools
 53 | 
 54 | - `get-entity-metadata`: Get metadata about a PowerPlatform entity
 55 | - `get-entity-attributes`: Get attributes/fields of a PowerPlatform entity
 56 | - `get-entity-attribute`: Get a specific attribute/field of a PowerPlatform entity
 57 | - `get-entity-relationships`: Get relationships for a PowerPlatform entity
 58 | - `get-global-option-set`: Get a global option set definition
 59 | - `get-record`: Get a specific record by entity name and ID
 60 | - `query-records`: Query records using an OData filter expression
 61 | - `use-powerplatform-prompt`: Use pre-defined prompt templates for PowerPlatform entities
 62 | 
 63 | ## MCP Prompts
 64 | 
 65 | The server includes a prompts feature that provides formatted, context-rich information about PowerPlatform entities.
 66 | 
 67 | ### Available Prompt Types
 68 | 
 69 | The `use-powerplatform-prompt` tool supports the following prompt types:
 70 | 
 71 | 1. **ENTITY_OVERVIEW**: Comprehensive overview of an entity
 72 | 2. **ATTRIBUTE_DETAILS**: Detailed information about a specific entity attribute
 73 | 3. **QUERY_TEMPLATE**: OData query template for an entity with example filters
 74 | 4. **RELATIONSHIP_MAP**: Visual map of entity relationships
 75 | 
 76 | ### Examples
 77 | 
 78 | #### Entity Overview Prompt
 79 | 
 80 | ```javascript
 81 | // Example client code
 82 | await mcpClient.invoke("use-powerplatform-prompt", {
 83 |   promptType: "ENTITY_OVERVIEW",
 84 |   entityName: "account"
 85 | });
 86 | ```
 87 | 
 88 | **Output:**
 89 | ```
 90 | ## Power Platform Entity: account
 91 | 
 92 | This is an overview of the 'account' entity in Microsoft Power Platform/Dataverse:
 93 | 
 94 | ### Entity Details
 95 | - Display Name: Account
 96 | - Schema Name: Account
 97 | - Description: Business that represents a customer or potential customer
 98 | - Primary Key: accountid
 99 | - Primary Name: name
100 | 
101 | ### Key Attributes
102 | - name: String (Account Name)
103 | - emailaddress1: String (Email)
104 | - telephone1: String (Main Phone)
105 | - address1_city: String (City)
106 | - statecode: Status (Status)
107 | - accountnumber: String (Account Number)
108 | - industrycode: OptionSetValue (Industry)
109 | - revenue: Money (Annual Revenue)
110 | - ownerid: Owner (Owner)
111 | - createdon: DateTime (Created On)
112 | 
113 | ### Relationships
114 | - One-to-Many Relationships: 42
115 | - Many-to-Many Relationships: 8
116 | 
117 | You can query this entity using OData filters against the plural name.
118 | ```
119 | 
120 | #### Attribute Details Prompt
121 | 
122 | ```javascript
123 | await mcpClient.invoke("use-powerplatform-prompt", {
124 |   promptType: "ATTRIBUTE_DETAILS",
125 |   entityName: "account",
126 |   attributeName: "revenue"
127 | });
128 | ```
129 | 
130 | **Output:**
131 | ```
132 | ## Attribute: revenue
133 | 
134 | Details for the 'revenue' attribute of the 'account' entity:
135 | 
136 | - Display Name: Annual Revenue
137 | - Description: Annual revenue for the account
138 | - Type: Money
139 | - Format: Currency
140 | - Is Required: No
141 | - Is Searchable: true
142 | 
143 | ### Usage Notes
144 | - Data Type: Money
145 | - Required: No
146 | - Max Length: N/A
147 | ```
148 | 
149 | #### Query Template Prompt
150 | 
151 | ```javascript
152 | await mcpClient.invoke("use-powerplatform-prompt", {
153 |   promptType: "QUERY_TEMPLATE",
154 |   entityName: "account"
155 | });
156 | ```
157 | 
158 | **Output:**
159 | ```
160 | ## OData Query Template for accounts
161 | 
162 | Use this template to build queries against the accounts entity:
163 | accounts?$select=name,emailaddress1,telephone1, address1_city,statecode&$filter=name eq 'Example'&$orderby=name asc&$top=50
164 | ```
165 | 
166 | ### Common Filter Examples
167 | - Equals: `name eq 'Contoso'`
168 | - Contains: `contains(name, 'Contoso')`
169 | - Greater than date: `createdon gt 2023-01-01T00:00:00Z`
170 | - Multiple conditions: `name eq 'Contoso' and statecode eq 0`
171 | ```
172 | 
173 | #### Relationship Map Prompt
174 | 
175 | ```javascript
176 | await mcpClient.invoke("use-powerplatform-prompt", {
177 |   promptType: "RELATIONSHIP_MAP",
178 |   entityName: "contact"
179 | });
180 | ```
181 | 
182 | **Output:**
183 | ```
184 | ## Relationship Map for contact
185 | 
186 | This shows all relationships for the 'contact' entity:
187 | 
188 | ### One-to-Many Relationships (contact as Primary)
189 | - contact_activity_parties: contact (1) → activityparty (N)
190 | - contact_connections1: contact (1) → connection (N)
191 | - contact_connections2: contact (1) → connection (N)
192 | - contact_customer_contacts: contact (1) → contact (N)
193 | - contact_master_contact: contact (1) → contact (N)
194 | 
195 | ### One-to-Many Relationships (contact as Related)
196 | - account_primary_contact: account (1) → contact (N)
197 | - customer_contacts: customer (1) → contact (N)
198 | - system_user_contacts: systemuser (1) → contact (N)
199 | 
200 | ### Many-to-Many Relationships
201 | - contactleads_association: contact (N) ↔ lead (N)
202 | - contactopportunities_association: contact (N) ↔ opportunity (N)
203 | - contactquotes_association: contact (N) ↔ quote (N)
204 | ```
205 | 
206 | ## Prompt Examples
207 | 
208 | Here are some examples of how you can interact with assistant using the PowerPlatform MCP prompts:
209 | 
210 | ### 1. Entity Understanding
211 | 
212 | You can ask assistant about entities in natural language:
213 | 
214 | ```
215 | User: "Tell me about the Account entity in our PowerPlatform environment"
216 | 
217 | Response:
218 | [uses ENTITY_OVERVIEW prompt internally]
219 | 
220 | Here's what I found about the Account entity:
221 | - It's a core business entity representing customers and potential customers
222 | - The primary identifier is 'accountid' and the primary name field is 'name'
223 | - Key fields include:
224 |   * name (Account Name)
225 |   * emailaddress1 (Email)
226 |   * telephone1 (Main Phone)
227 |   * revenue (Annual Revenue)
228 | - It has 42 one-to-many relationships and 8 many-to-many relationships
229 | ```
230 | 
231 | ### 2. Building Queries
232 | 
233 | You can ask for help constructing queries:
234 | 
235 | ```
236 | User: "Help me write a query to find active accounts with revenue over $1M in California"
237 | 
238 | Response:
239 | [uses QUERY_TEMPLATE prompt internally]
240 | 
241 | Here's the OData query you need:
242 | accounts?$select=name,revenue,statecode,address1_stateorprovince
243 | &$filter=statecode eq 0 
244 |     and revenue gt 1000000 
245 |     and address1_stateorprovince eq 'California'
246 | &$orderby=revenue desc
247 | 
248 | This query:
249 | 1. Selects relevant fields
250 | 2. Filters for active accounts (statecode eq 0)
251 | 3. Checks revenue > $1M
252 | 4. Filters for California location
253 | 5. Orders by revenue highest to lowest
254 | ```
255 | 
256 | ### 3. Understanding Relationships
257 | 
258 | You can explore entity relationships naturally:
259 | 
260 | ```
261 | User: "How are Accounts and Contacts related in our system?"
262 | 
263 | Response:
264 | [uses RELATIONSHIP_MAP prompt for both entities internally]
265 | 
266 | Here are the key relationships between Accounts and Contacts:
267 | 
268 | 1. Primary Contact (One-to-Many):
269 |    - account_primary_contact: Account (1) → Contact (N)
270 |    - An account can have many contacts, but one designated as primary
271 | 
272 | 2. Customer Relationship:
273 |    - contact_customer_accounts: Contact (N) ← Account (1)
274 |    - Contacts can be associated with customer accounts
275 | 
276 | 3. Additional Relationships:
277 |    - accountleads_association: For lead tracking
278 |    - account_connections: For relationship management
279 | ```
280 | 
281 | ### 4. Field/Attribute Investigation
282 | 
283 | You can ask about specific fields:
284 | 
285 | ```
286 | User: "What kind of field is the revenue field on Account?"
287 | 
288 | Response:
289 | [uses ATTRIBUTE_DETAILS prompt internally]
290 | 
291 | The revenue field on Account is:
292 | - Data Type: Money
293 | - Format: Currency
294 | - Not required
295 | - Searchable: Yes
296 | - Used for: Tracking annual revenue for the account
297 | - Can be used in advanced find and filtered views
298 | ```
299 | 
300 | ### 5. Complex Data Modeling
301 | 
302 | You can get help with data modeling decisions:
303 | 
304 | ```
305 | User: "I need to design a solution for tracking product reviews. Which entities should I relate to?"
306 | 
307 | Response:
308 | [uses multiple ENTITY_OVERVIEW and RELATIONSHIP_MAP prompts internally]
309 | 
310 | Based on the existing data model, here's a suggested approach:
311 | 
312 | 1. Product (existing entity):
313 |    - Already has relationships with:
314 |      * Price lists
315 |      * Product categories
316 |      * Inventory units
317 | 
318 | 2. New Review entity should relate to:
319 |    - Product (N:1) - Each review is for one product
320 |    - Contact (N:1) - Reviews are written by contacts
321 |    - Account (N:1) - Optional link to company if B2B review
322 | 
323 | Here's a sample lookup field structure...
324 | [continues with detailed implementation guidance]
325 | ```
326 | 
327 | ### 6. Debugging Assistance
328 | 
329 | You can get help troubleshooting:
330 | 
331 | ```
332 | User: "Why am I getting 'invalid property' when querying account.customertypecode?"
333 | 
334 | Response:
335 | [uses ATTRIBUTE_DETAILS prompt internally]
336 | 
337 | I see the issue. The field 'customertypecode' is actually named 'customertype' in your environment. Here's the correct field information:
338 | - Logical Name: customertype
339 | - Type: OptionSetValue
340 | - Valid Values:
341 |   * 1 = Competitor
342 |   * 2 = Consultant
343 |   * 3 = Customer
344 |   * 4 = Investor
345 |   * 5 = Partner
346 |   * 6 = Influencer
347 | 
348 | Try updating your query to use 'customertype' instead of 'customertypecode'.
349 | ```
350 | 
351 | 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.
352 | 
353 | ## License
354 | 
355 | MIT
356 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "compilerOptions": {
 3 |       "target": "ES2022",
 4 |       "module": "Node16",
 5 |       "moduleResolution": "Node16",
 6 |       "outDir": "./build",
 7 |       "rootDir": "./src",
 8 |       "strict": true,
 9 |       "esModuleInterop": true,
10 |       "skipLibCheck": true,
11 |       "forceConsistentCasingInFileNames": true
12 |     },
13 |     "include": ["src/**/*"],
14 |     "exclude": ["node_modules"]
15 |   }
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "powerplatform-mcp",
 3 |   "version": "0.4.5",
 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 | }
48 | 
```

--------------------------------------------------------------------------------
/src/PowerPlatformService.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ConfidentialClientApplication } from '@azure/msal-node';
  2 | import axios from 'axios';
  3 | 
  4 | export interface PowerPlatformConfig {
  5 |   organizationUrl: string;
  6 |   clientId: string;
  7 |   clientSecret: string;
  8 |   tenantId: string;
  9 | }
 10 | 
 11 | // Interface for API responses with value collections
 12 | export interface ApiCollectionResponse<T> {
 13 |   value: T[];
 14 |   [key: string]: any; // For any additional properties
 15 | }
 16 | 
 17 | export class PowerPlatformService {
 18 |   private config: PowerPlatformConfig;
 19 |   private msalClient: ConfidentialClientApplication;
 20 |   private accessToken: string | null = null;
 21 |   private tokenExpirationTime: number = 0;
 22 | 
 23 |   constructor(config: PowerPlatformConfig) {
 24 |     this.config = config;
 25 |     
 26 |     // Initialize MSAL client
 27 |     this.msalClient = new ConfidentialClientApplication({
 28 |       auth: {
 29 |         clientId: this.config.clientId,
 30 |         clientSecret: this.config.clientSecret,
 31 |         authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
 32 |       }
 33 |     });
 34 |   }
 35 | 
 36 |   /**
 37 |    * Get an access token for the PowerPlatform API
 38 |    */
 39 |   private async getAccessToken(): Promise<string> {
 40 |     const currentTime = Date.now();
 41 |     
 42 |     // If we have a token that isn't expired, return it
 43 |     if (this.accessToken && this.tokenExpirationTime > currentTime) {
 44 |       return this.accessToken;
 45 |     }
 46 | 
 47 |     try {
 48 |       // Get a new token
 49 |       const result = await this.msalClient.acquireTokenByClientCredential({
 50 |         scopes: [`${this.config.organizationUrl}/.default`],
 51 |       });
 52 | 
 53 |       if (!result || !result.accessToken) {
 54 |         throw new Error('Failed to acquire access token');
 55 |       }
 56 | 
 57 |       this.accessToken = result.accessToken;
 58 |       
 59 |       // Set expiration time (subtract 5 minutes to refresh early)
 60 |       if (result.expiresOn) {
 61 |         this.tokenExpirationTime = result.expiresOn.getTime() - (5 * 60 * 1000);
 62 |       }
 63 | 
 64 |       return this.accessToken;
 65 |     } catch (error) {
 66 |       console.error('Error acquiring access token:', error);
 67 |       throw new Error('Authentication failed');
 68 |     }
 69 |   }
 70 | 
 71 |   /**
 72 |    * Make an authenticated request to the PowerPlatform API
 73 |    */
 74 |   private async makeRequest<T>(endpoint: string): Promise<T> {
 75 |     try {
 76 |       const token = await this.getAccessToken();
 77 | 
 78 |       const response = await axios({
 79 |         method: 'GET',
 80 |         url: `${this.config.organizationUrl}/${endpoint}`,
 81 |         headers: {
 82 |           'Authorization': `Bearer ${token}`,
 83 |           'Accept': 'application/json',
 84 |           'OData-MaxVersion': '4.0',
 85 |           'OData-Version': '4.0'
 86 |         }
 87 |       });
 88 | 
 89 |       return response.data as T;
 90 |     } catch (error) {
 91 |       console.error('PowerPlatform API request failed:', error);
 92 |       throw new Error(`PowerPlatform API request failed: ${error}`);
 93 |     }
 94 |   }
 95 | 
 96 |   /**
 97 |    * Get metadata about an entity
 98 |    * @param entityName The logical name of the entity
 99 |    */
100 |   async getEntityMetadata(entityName: string): Promise<any> {
101 |     const response = await this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')`);
102 |     
103 |     // Remove Privileges property if it exists
104 |     if (response && typeof response === 'object' && 'Privileges' in response) {
105 |       delete response.Privileges;
106 |     }
107 |     
108 |     return response;
109 |   }
110 | 
111 |   /**
112 |    * Get metadata about entity attributes/fields
113 |    * @param entityName The logical name of the entity
114 |    */
115 |   async getEntityAttributes(entityName: string): Promise<ApiCollectionResponse<any>> {
116 |     const selectProperties = [
117 |       'LogicalName',
118 |     ].join(',');
119 |     
120 |     // Make the request to get attributes
121 |     const response = await this.makeRequest<ApiCollectionResponse<any>>(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes?$select=${selectProperties}&$filter=AttributeType ne 'Virtual'`);
122 |     
123 |     if (response && response.value) {
124 |       // First pass: Filter out attributes that end with 'yominame'
125 |       response.value = response.value.filter((attribute: any) => {
126 |         const logicalName = attribute.LogicalName || '';
127 |         return !logicalName.endsWith('yominame');
128 |       });
129 |       
130 |       // Filter out attributes that end with 'name' if there is another attribute with the same name without the 'name' suffix
131 |       const baseNames = new Set<string>();
132 |       const namesAttributes = new Map<string, any>();
133 |       
134 |       for (const attribute of response.value) {
135 |         const logicalName = attribute.LogicalName || '';
136 |       
137 |         if (logicalName.endsWith('name') && logicalName.length > 4) {
138 |           const baseName = logicalName.slice(0, -4); // Remove 'name' suffix
139 |           namesAttributes.set(baseName, attribute);
140 |         } else {
141 |           // This is a potential base attribute
142 |           baseNames.add(logicalName);
143 |         }
144 |       }
145 |       
146 |       // Find attributes to remove that match the pattern
147 |       const attributesToRemove = new Set<any>();
148 |       for (const [baseName, nameAttribute] of namesAttributes.entries()) {
149 |         if (baseNames.has(baseName)) {
150 |           attributesToRemove.add(nameAttribute);
151 |         }
152 |       }
153 | 
154 |       response.value = response.value.filter(attribute => !attributesToRemove.has(attribute));
155 |     }
156 |     
157 |     return response;
158 |   }
159 | 
160 |   /**
161 |    * Get metadata about a specific entity attribute/field
162 |    * @param entityName The logical name of the entity
163 |    * @param attributeName The logical name of the attribute
164 |    */
165 |   async getEntityAttribute(entityName: string, attributeName: string): Promise<any> {
166 |     return this.makeRequest(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes(LogicalName='${attributeName}')`);
167 |   }
168 | 
169 |   /**
170 |    * Get one-to-many relationships for an entity
171 |    * @param entityName The logical name of the entity
172 |    */
173 |   async getEntityOneToManyRelationships(entityName: string): Promise<ApiCollectionResponse<any>> {
174 |     const selectProperties = [
175 |       'SchemaName',
176 |       'RelationshipType',
177 |       'ReferencedAttribute',
178 |       'ReferencedEntity',
179 |       'ReferencingAttribute',
180 |       'ReferencingEntity',
181 |       'ReferencedEntityNavigationPropertyName',
182 |       'ReferencingEntityNavigationPropertyName'
183 |     ].join(',');
184 |     
185 |     // Only filter by ReferencingAttribute in the OData query since startswith isn't supported
186 |     const response = await this.makeRequest<ApiCollectionResponse<any>>(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/OneToManyRelationships?$select=${selectProperties}&$filter=ReferencingAttribute ne 'regardingobjectid'`);
187 |     
188 |     // Filter the response to exclude relationships with ReferencingEntity starting with 'msdyn_' or 'adx_'
189 |     if (response && response.value) {
190 |       response.value = response.value.filter((relationship: any) => {
191 |         const referencingEntity = relationship.ReferencingEntity || '';
192 |         return !(referencingEntity.startsWith('msdyn_') || referencingEntity.startsWith('adx_'));
193 |       });
194 |     }
195 |     
196 |     return response;
197 |   }
198 | 
199 |   /**
200 |    * Get many-to-many relationships for an entity
201 |    * @param entityName The logical name of the entity
202 |    */
203 |   async getEntityManyToManyRelationships(entityName: string): Promise<ApiCollectionResponse<any>> {
204 |     const selectProperties = [
205 |       'SchemaName',
206 |       'RelationshipType',
207 |       'Entity1LogicalName',
208 |       'Entity2LogicalName',
209 |       'Entity1IntersectAttribute',
210 |       'Entity2IntersectAttribute',
211 |       'Entity1NavigationPropertyName',
212 |       'Entity2NavigationPropertyName'
213 |     ].join(',');
214 |     
215 |     return this.makeRequest<ApiCollectionResponse<any>>(`api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/ManyToManyRelationships?$select=${selectProperties}`);
216 |   }
217 | 
218 |   /**
219 |    * Get all relationships (one-to-many and many-to-many) for an entity
220 |    * @param entityName The logical name of the entity
221 |    */
222 |   async getEntityRelationships(entityName: string): Promise<{oneToMany: ApiCollectionResponse<any>, manyToMany: ApiCollectionResponse<any>}> {
223 |     const [oneToMany, manyToMany] = await Promise.all([
224 |       this.getEntityOneToManyRelationships(entityName),
225 |       this.getEntityManyToManyRelationships(entityName)
226 |     ]);
227 |     
228 |     return {
229 |       oneToMany,
230 |       manyToMany
231 |     };
232 |   }
233 | 
234 |   /**
235 |    * Get a global option set definition by name
236 |    * @param optionSetName The name of the global option set
237 |    * @returns The global option set definition
238 |    */
239 |   async getGlobalOptionSet(optionSetName: string): Promise<any> {
240 |     return this.makeRequest(`api/data/v9.2/GlobalOptionSetDefinitions(Name='${optionSetName}')`);
241 |   }
242 | 
243 |   /**
244 |    * Get a specific record by entity name (plural) and ID
245 |    * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
246 |    * @param recordId The GUID of the record
247 |    * @returns The record data
248 |    */
249 |   async getRecord(entityNamePlural: string, recordId: string): Promise<any> {
250 |     return this.makeRequest(`api/data/v9.2/${entityNamePlural}(${recordId})`);
251 |   }
252 | 
253 |   /**
254 |    * Query records using entity name (plural) and a filter expression
255 |    * @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
256 |    * @param filter OData filter expression (e.g., "name eq 'test'")
257 |    * @param maxRecords Maximum number of records to retrieve (default: 50)
258 |    * @returns Filtered list of records
259 |    */
260 |   async queryRecords(entityNamePlural: string, filter: string, maxRecords: number = 50): Promise<ApiCollectionResponse<any>> {
261 |     return this.makeRequest<ApiCollectionResponse<any>>(`api/data/v9.2/${entityNamePlural}?$filter=${encodeURIComponent(filter)}&$top=${maxRecords}`);
262 |   }
263 | }
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  4 | import { z } from "zod";
  5 | import { PowerPlatformService, PowerPlatformConfig } from "./PowerPlatformService.js";
  6 | 
  7 | // Environment configuration
  8 | // These values can be set in environment variables or loaded from a configuration file
  9 | const POWERPLATFORM_CONFIG: PowerPlatformConfig = {
 10 |   organizationUrl: process.env.POWERPLATFORM_URL || "",
 11 |   clientId: process.env.POWERPLATFORM_CLIENT_ID || "",
 12 |   clientSecret: process.env.POWERPLATFORM_CLIENT_SECRET || "",
 13 |   tenantId: process.env.POWERPLATFORM_TENANT_ID || "",
 14 | };
 15 | 
 16 | // Create server instance
 17 | const server = new McpServer({
 18 |   name: "powerplatform-mcp",
 19 |   version: "1.0.0",
 20 | });
 21 | 
 22 | let powerPlatformService: PowerPlatformService | null = null;
 23 | 
 24 | // Function to initialize PowerPlatformService on demand
 25 | function getPowerPlatformService(): PowerPlatformService {
 26 |   if (!powerPlatformService) {
 27 |     // Check if configuration is complete
 28 |     const missingConfig: string[] = [];
 29 |     if (!POWERPLATFORM_CONFIG.organizationUrl) missingConfig.push("organizationUrl");
 30 |     if (!POWERPLATFORM_CONFIG.clientId) missingConfig.push("clientId");
 31 |     if (!POWERPLATFORM_CONFIG.clientSecret) missingConfig.push("clientSecret");
 32 |     if (!POWERPLATFORM_CONFIG.tenantId) missingConfig.push("tenantId");
 33 |     
 34 |     if (missingConfig.length > 0) {
 35 |       throw new Error(`Missing PowerPlatform configuration: ${missingConfig.join(", ")}. Set these in environment variables.`);
 36 |     }
 37 |     
 38 |     // Initialize service
 39 |     powerPlatformService = new PowerPlatformService(POWERPLATFORM_CONFIG);
 40 |     console.error("PowerPlatform service initialized");
 41 |   }
 42 |   
 43 |   return powerPlatformService;
 44 | }
 45 | 
 46 | // Pre-defined PowerPlatform Prompts
 47 | const powerPlatformPrompts = {
 48 |   // Entity exploration prompts
 49 |   ENTITY_OVERVIEW: (entityName: string) => 
 50 |     `## Power Platform Entity: ${entityName}\n\n` +
 51 |     `This is an overview of the '${entityName}' entity in Microsoft Power Platform/Dataverse:\n\n` +
 52 |     `### Entity Details\n{{entity_details}}\n\n` +
 53 |     `### Attributes\n{{key_attributes}}\n\n` +
 54 |     `### Relationships\n{{relationships}}\n\n` +
 55 |     `You can query this entity using OData filters against the plural name.`,
 56 | 
 57 |   ATTRIBUTE_DETAILS: (entityName: string, attributeName: string) =>
 58 |     `## Attribute: ${attributeName}\n\n` +
 59 |     `Details for the '${attributeName}' attribute of the '${entityName}' entity:\n\n` +
 60 |     `{{attribute_details}}\n\n` +
 61 |     `### Usage Notes\n` +
 62 |     `- Data Type: {{data_type}}\n` +
 63 |     `- Required: {{required}}\n` +
 64 |     `- Max Length: {{max_length}}`,
 65 | 
 66 |   // Query builder prompts
 67 |   QUERY_TEMPLATE: (entityNamePlural: string) =>
 68 |     `## OData Query Template for ${entityNamePlural}\n\n` +
 69 |     `Use this template to build queries against the ${entityNamePlural} entity:\n\n` +
 70 |     `\`\`\`\n${entityNamePlural}?$select={{selected_fields}}&$filter={{filter_conditions}}&$orderby={{order_by}}&$top={{max_records}}\n\`\`\`\n\n` +
 71 |     `### Common Filter Examples\n` +
 72 |     `- Equals: \`name eq 'Contoso'\`\n` +
 73 |     `- Contains: \`contains(name, 'Contoso')\`\n` +
 74 |     `- Greater than date: \`createdon gt 2023-01-01T00:00:00Z\`\n` +
 75 |     `- Multiple conditions: \`name eq 'Contoso' and statecode eq 0\``,
 76 | 
 77 |   // Relationship exploration prompts
 78 |   RELATIONSHIP_MAP: (entityName: string) =>
 79 |     `## Relationship Map for ${entityName}\n\n` +
 80 |     `This shows all relationships for the '${entityName}' entity:\n\n` +
 81 |     `### One-to-Many Relationships (${entityName} as Primary)\n{{one_to_many_primary}}\n\n` +
 82 |     `### One-to-Many Relationships (${entityName} as Related)\n{{one_to_many_related}}\n\n` +
 83 |     `### Many-to-Many Relationships\n{{many_to_many}}\n\n`
 84 | };
 85 | 
 86 | // Register prompts with the server using the correct method signature
 87 | // Entity Overview Prompt
 88 | server.prompt(
 89 |   "entity-overview", 
 90 |   "Get an overview of a Power Platform entity",
 91 |   {
 92 |     entityName: z.string().describe("The logical name of the entity")
 93 |   },
 94 |   async (args) => {
 95 |     try {
 96 |       const service = getPowerPlatformService();
 97 |       const entityName = args.entityName;
 98 |       
 99 |       // Get entity metadata and key attributes
100 |       const [metadata, attributes] = await Promise.all([
101 |         service.getEntityMetadata(entityName),
102 |         service.getEntityAttributes(entityName)
103 |       ]);
104 |       
105 |       // Format entity details
106 |       const entityDetails = `- Display Name: ${metadata.DisplayName?.UserLocalizedLabel?.Label || entityName}\n` +
107 |         `- Schema Name: ${metadata.SchemaName}\n` +
108 |         `- Description: ${metadata.Description?.UserLocalizedLabel?.Label || 'No description'}\n` +
109 |         `- Primary Key: ${metadata.PrimaryIdAttribute}\n` +
110 |         `- Primary Name: ${metadata.PrimaryNameAttribute}`;
111 |         
112 |       // Get key attributes
113 |       const keyAttributes = attributes.value
114 |         .map((attr: any) => {
115 |           const attrType = attr["@odata.type"] || attr.odata?.type || "Unknown type";
116 |           return `- ${attr.LogicalName}: ${attrType}`;
117 |         })
118 |         .join('\n');
119 |         
120 |       // Get relationships summary
121 |       const relationships = await service.getEntityRelationships(entityName);
122 |       const oneToManyCount = relationships.oneToMany.value.length;
123 |       const manyToManyCount = relationships.manyToMany.value.length;
124 |       
125 |       const relationshipsSummary = `- One-to-Many Relationships: ${oneToManyCount}\n` +
126 |                                   `- Many-to-Many Relationships: ${manyToManyCount}`;
127 |       
128 |       let promptContent = powerPlatformPrompts.ENTITY_OVERVIEW(entityName);
129 |       promptContent = promptContent
130 |         .replace('{{entity_details}}', entityDetails)
131 |         .replace('{{key_attributes}}', keyAttributes)
132 |         .replace('{{relationships}}', relationshipsSummary);
133 |       
134 |       return {
135 |         messages: [
136 |           {
137 |             role: "assistant",
138 |             content: {
139 |               type: "text",
140 |               text: promptContent
141 |             }
142 |           }
143 |         ]
144 |       };
145 |     } catch (error: any) {
146 |       console.error(`Error handling entity-overview prompt:`, error);
147 |       return {
148 |         messages: [
149 |           {
150 |             role: "assistant",
151 |             content: {
152 |               type: "text",
153 |               text: `Error: ${error.message}`
154 |             }
155 |           }
156 |         ]
157 |       };
158 |     }
159 |   }
160 | );
161 | 
162 | // Attribute Details Prompt
163 | server.prompt(
164 |   "attribute-details",
165 |   "Get detailed information about a specific entity attribute/field",
166 |   {
167 |     entityName: z.string().describe("The logical name of the entity"),
168 |     attributeName: z.string().describe("The logical name of the attribute"),
169 |   },
170 |   async (args) => {
171 |     try {
172 |       const service = getPowerPlatformService();
173 |       const { entityName, attributeName } = args;
174 |       
175 |       // Get attribute details
176 |       const attribute = await service.getEntityAttribute(entityName, attributeName);
177 |       
178 |       // Format attribute details
179 |       const attrDetails = `- Display Name: ${attribute.DisplayName?.UserLocalizedLabel?.Label || attributeName}\n` +
180 |         `- Description: ${attribute.Description?.UserLocalizedLabel?.Label || 'No description'}\n` +
181 |         `- Type: ${attribute.AttributeType}\n` +
182 |         `- Format: ${attribute.Format || 'N/A'}\n` +
183 |         `- Is Required: ${attribute.RequiredLevel?.Value || 'No'}\n` +
184 |         `- Is Searchable: ${attribute.IsValidForAdvancedFind || false}`;
185 |         
186 |       let promptContent = powerPlatformPrompts.ATTRIBUTE_DETAILS(entityName, attributeName);
187 |       promptContent = promptContent
188 |         .replace('{{attribute_details}}', attrDetails)
189 |         .replace('{{data_type}}', attribute.AttributeType)
190 |         .replace('{{required}}', attribute.RequiredLevel?.Value || 'No')
191 |         .replace('{{max_length}}', attribute.MaxLength || 'N/A');
192 |       
193 |       return {
194 |         messages: [
195 |           {
196 |             role: "assistant",
197 |             content: {
198 |               type: "text",
199 |               text: promptContent
200 |             }
201 |           }
202 |         ]
203 |       };
204 |     } catch (error: any) {
205 |       console.error(`Error handling attribute-details prompt:`, error);
206 |       return {
207 |         messages: [
208 |           {
209 |             role: "assistant",
210 |             content: {
211 |               type: "text",
212 |               text: `Error: ${error.message}`
213 |             }
214 |           }
215 |         ]
216 |       };
217 |     }
218 |   }
219 | );
220 | 
221 | // Query Template Prompt
222 | server.prompt(
223 |   "query-template",
224 |   "Get a template for querying a Power Platform entity",
225 |   {
226 |     entityName: z.string().describe("The logical name of the entity"),
227 |   },
228 |   async (args) => {
229 |     try {
230 |       const service = getPowerPlatformService();
231 |       const entityName = args.entityName;
232 |       
233 |       // Get entity metadata to determine plural name
234 |       const metadata = await service.getEntityMetadata(entityName);
235 |       const entityNamePlural = metadata.EntitySetName;
236 |       
237 |       // Get a few important fields for the select example
238 |       const attributes = await service.getEntityAttributes(entityName);
239 |       const selectFields = attributes.value
240 |         .filter((attr: any) => attr.IsValidForRead === true && !attr.AttributeOf)
241 |         .slice(0, 5) // Just take first 5 for example
242 |         .map((attr: any) => attr.LogicalName)
243 |         .join(',');
244 |         
245 |       let promptContent = powerPlatformPrompts.QUERY_TEMPLATE(entityNamePlural);
246 |       promptContent = promptContent
247 |         .replace('{{selected_fields}}', selectFields)
248 |         .replace('{{filter_conditions}}', `${metadata.PrimaryNameAttribute} eq 'Example'`)
249 |         .replace('{{order_by}}', `${metadata.PrimaryNameAttribute} asc`)
250 |         .replace('{{max_records}}', '50');
251 |       
252 |       return {
253 |         messages: [
254 |           {
255 |             role: "assistant",
256 |             content: {
257 |               type: "text",
258 |               text: promptContent
259 |             }
260 |           }
261 |         ]
262 |       };
263 |     } catch (error: any) {
264 |       console.error(`Error handling query-template prompt:`, error);
265 |       return {
266 |         messages: [
267 |           {
268 |             role: "assistant",
269 |             content: {
270 |               type: "text",
271 |               text: `Error: ${error.message}`
272 |             }
273 |           }
274 |         ]
275 |       };
276 |     }
277 |   }
278 | );
279 | 
280 | // Relationship Map Prompt
281 | server.prompt(
282 |   "relationship-map",
283 |   "Get a list of relationships for a Power Platform entity",
284 |   {
285 |     entityName: z.string().describe("The logical name of the entity"),
286 |   },
287 |   async (args) => {
288 |     try {
289 |       const service = getPowerPlatformService();
290 |       const entityName = args.entityName;
291 |       
292 |       // Get relationships
293 |       const relationships = await service.getEntityRelationships(entityName);
294 |       
295 |       // Format one-to-many relationships where this entity is primary
296 |       const oneToManyPrimary = relationships.oneToMany.value
297 |         .filter((rel: any) => rel.ReferencingEntity !== entityName)
298 |         .map((rel: any) => `- ${rel.SchemaName}: ${entityName} (1) → ${rel.ReferencingEntity} (N)`)
299 |         .join('\n');
300 |         
301 |       // Format one-to-many relationships where this entity is related
302 |       const oneToManyRelated = relationships.oneToMany.value
303 |         .filter((rel: any) => rel.ReferencingEntity === entityName)
304 |         .map((rel: any) => `- ${rel.SchemaName}: ${rel.ReferencedEntity} (1) → ${entityName} (N)`)
305 |         .join('\n');
306 |         
307 |       // Format many-to-many relationships
308 |       const manyToMany = relationships.manyToMany.value
309 |         .map((rel: any) => {
310 |           const otherEntity = rel.Entity1LogicalName === entityName ? rel.Entity2LogicalName : rel.Entity1LogicalName;
311 |           return `- ${rel.SchemaName}: ${entityName} (N) ↔ ${otherEntity} (N)`;
312 |         })
313 |         .join('\n');
314 |       
315 |       let promptContent = powerPlatformPrompts.RELATIONSHIP_MAP(entityName);
316 |       promptContent = promptContent
317 |         .replace('{{one_to_many_primary}}', oneToManyPrimary || 'None found')
318 |         .replace('{{one_to_many_related}}', oneToManyRelated || 'None found')
319 |         .replace('{{many_to_many}}', manyToMany || 'None found');
320 |       
321 |       return {
322 |         messages: [
323 |           {
324 |             role: "assistant",
325 |             content: {
326 |               type: "text",
327 |               text: promptContent
328 |             }
329 |           }
330 |         ]
331 |       };
332 |     } catch (error: any) {
333 |       console.error(`Error handling relationship-map prompt:`, error);
334 |       return {
335 |         messages: [
336 |           {
337 |             role: "assistant",
338 |             content: {
339 |               type: "text",
340 |               text: `Error: ${error.message}`
341 |             }
342 |           }
343 |         ]
344 |       };
345 |     }
346 |   }
347 | );
348 | 
349 | // PowerPlatform entity metadata
350 | server.tool(
351 |   "get-entity-metadata",
352 |   "Get metadata about a PowerPlatform entity",
353 |   {
354 |     entityName: z.string().describe("The logical name of the entity"),
355 |   },
356 |   async ({ entityName }) => {
357 |     try {
358 |       // Get or initialize PowerPlatformService
359 |       const service = getPowerPlatformService();
360 |       const metadata = await service.getEntityMetadata(entityName);
361 |       
362 |       // Format the metadata as a string for text display
363 |       const metadataStr = JSON.stringify(metadata, null, 2);
364 |       
365 |       return {
366 |         content: [
367 |           {
368 |             type: "text",
369 |             text: `Entity metadata for '${entityName}':\n\n${metadataStr}`,
370 |           },
371 |         ],
372 |       };
373 |     } catch (error: any) {
374 |       console.error("Error getting entity metadata:", error);
375 |       return {
376 |         content: [
377 |           {
378 |             type: "text",
379 |             text: `Failed to get entity metadata: ${error.message}`,
380 |           },
381 |         ],
382 |       };
383 |     }
384 |   }
385 | );
386 | 
387 | // PowerPlatform entity attributes
388 | server.tool(
389 |   "get-entity-attributes",
390 |   "Get attributes/fields of a PowerPlatform entity",
391 |   {
392 |     entityName: z.string().describe("The logical name of the entity"),
393 |   },
394 |   async ({ entityName }) => {
395 |     try {
396 |       // Get or initialize PowerPlatformService
397 |       const service = getPowerPlatformService();
398 |       const attributes = await service.getEntityAttributes(entityName);
399 |       
400 |       // Format the attributes as a string for text display
401 |       const attributesStr = JSON.stringify(attributes, null, 2);
402 |       
403 |       return {
404 |         content: [
405 |           {
406 |             type: "text",
407 |             text: `Attributes for entity '${entityName}':\n\n${attributesStr}`,
408 |           },
409 |         ],
410 |       };
411 |     } catch (error: any) {
412 |       console.error("Error getting entity attributes:", error);
413 |       return {
414 |         content: [
415 |           {
416 |             type: "text",
417 |             text: `Failed to get entity attributes: ${error.message}`,
418 |           },
419 |         ],
420 |       };
421 |     }
422 |   }
423 | );
424 | 
425 | // PowerPlatform specific entity attribute
426 | server.tool(
427 |   "get-entity-attribute",
428 |   "Get a specific attribute/field of a PowerPlatform entity",
429 |   {
430 |     entityName: z.string().describe("The logical name of the entity"),
431 |     attributeName: z.string().describe("The logical name of the attribute")
432 |   },
433 |   async ({ entityName, attributeName }) => {
434 |     try {
435 |       // Get or initialize PowerPlatformService
436 |       const service = getPowerPlatformService();
437 |       const attribute = await service.getEntityAttribute(entityName, attributeName);
438 |       
439 |       // Format the attribute as a string for text display
440 |       const attributeStr = JSON.stringify(attribute, null, 2);
441 |       
442 |       return {
443 |         content: [
444 |           {
445 |             type: "text",
446 |             text: `Attribute '${attributeName}' for entity '${entityName}':\n\n${attributeStr}`,
447 |           },
448 |         ],
449 |       };
450 |     } catch (error: any) {
451 |       console.error("Error getting entity attribute:", error);
452 |       return {
453 |         content: [
454 |           {
455 |             type: "text",
456 |             text: `Failed to get entity attribute: ${error.message}`,
457 |           },
458 |         ],
459 |       };
460 |     }
461 |   }
462 | );
463 | 
464 | // PowerPlatform entity relationships
465 | server.tool(
466 |   "get-entity-relationships",
467 |   "Get relationships (one-to-many and many-to-many) for a PowerPlatform entity",
468 |   {
469 |     entityName: z.string().describe("The logical name of the entity"),
470 |   },
471 |   async ({ entityName }) => {
472 |     try {
473 |       // Get or initialize PowerPlatformService
474 |       const service = getPowerPlatformService();
475 |       const relationships = await service.getEntityRelationships(entityName);
476 |       
477 |       // Format the relationships as a string for text display
478 |       const relationshipsStr = JSON.stringify(relationships, null, 2);
479 |       
480 |       return {
481 |         content: [
482 |           {
483 |             type: "text",
484 |             text: `Relationships for entity '${entityName}':\n\n${relationshipsStr}`,
485 |           },
486 |         ],
487 |       };
488 |     } catch (error: any) {
489 |       console.error("Error getting entity relationships:", error);
490 |       return {
491 |         content: [
492 |           {
493 |             type: "text",
494 |             text: `Failed to get entity relationships: ${error.message}`,
495 |           },
496 |         ],
497 |       };
498 |     }
499 |   }
500 | );
501 | 
502 | // PowerPlatform global option set
503 | server.tool(
504 |   "get-global-option-set",
505 |   "Get a global option set definition by name",
506 |   {
507 |     optionSetName: z.string().describe("The name of the global option set"),
508 |   },
509 |   async ({ optionSetName }) => {
510 |     try {
511 |       // Get or initialize PowerPlatformService
512 |       const service = getPowerPlatformService();
513 |       const optionSet = await service.getGlobalOptionSet(optionSetName);
514 |       
515 |       // Format the option set as a string for text display
516 |       const optionSetStr = JSON.stringify(optionSet, null, 2);
517 |       
518 |       return {
519 |         content: [
520 |           {
521 |             type: "text",
522 |             text: `Global option set '${optionSetName}':\n\n${optionSetStr}`,
523 |           },
524 |         ],
525 |       };
526 |     } catch (error: any) {
527 |       console.error("Error getting global option set:", error);
528 |       return {
529 |         content: [
530 |           {
531 |             type: "text",
532 |             text: `Failed to get global option set: ${error.message}`,
533 |           },
534 |         ],
535 |       };
536 |     }
537 |   }
538 | );
539 | 
540 | // PowerPlatform record by ID
541 | server.tool(
542 |   "get-record",
543 |   "Get a specific record by entity name (plural) and ID",
544 |   {
545 |     entityNamePlural: z.string().describe("The plural name of the entity (e.g., 'accounts', 'contacts')"),
546 |     recordId: z.string().describe("The GUID of the record"),
547 |   },
548 |   async ({ entityNamePlural, recordId }) => {
549 |     try {
550 |       // Get or initialize PowerPlatformService
551 |       const service = getPowerPlatformService();
552 |       const record = await service.getRecord(entityNamePlural, recordId);
553 |       
554 |       // Format the record as a string for text display
555 |       const recordStr = JSON.stringify(record, null, 2);
556 |       
557 |       return {
558 |         content: [
559 |           {
560 |             type: "text",
561 |             text: `Record from '${entityNamePlural}' with ID '${recordId}':\n\n${recordStr}`,
562 |           },
563 |         ],
564 |       };
565 |     } catch (error: any) {
566 |       console.error("Error getting record:", error);
567 |       return {
568 |         content: [
569 |           {
570 |             type: "text",
571 |             text: `Failed to get record: ${error.message}`,
572 |           },
573 |         ],
574 |       };
575 |     }
576 |   }
577 | );
578 | 
579 | // PowerPlatform query records with filter
580 | server.tool(
581 |   "query-records",
582 |   "Query records using an OData filter expression",
583 |   {
584 |     entityNamePlural: z.string().describe("The plural name of the entity (e.g., 'accounts', 'contacts')"),
585 |     filter: z.string().describe("OData filter expression (e.g., \"name eq 'test'\" or \"createdon gt 2023-01-01\")"),
586 |     maxRecords: z.number().optional().describe("Maximum number of records to retrieve (default: 50)"),
587 |   },
588 |   async ({ entityNamePlural, filter, maxRecords }) => {
589 |     try {
590 |       // Get or initialize PowerPlatformService
591 |       const service = getPowerPlatformService();
592 |       const records = await service.queryRecords(entityNamePlural, filter, maxRecords || 50);
593 |       
594 |       // Format the records as a string for text display
595 |       const recordsStr = JSON.stringify(records, null, 2);
596 |       const recordCount = records.value?.length || 0;
597 |       
598 |       return {
599 |         content: [
600 |           {
601 |             type: "text",
602 |             text: `Retrieved ${recordCount} records from '${entityNamePlural}' with filter '${filter}':\n\n${recordsStr}`,
603 |           },
604 |         ],
605 |       };
606 |     } catch (error: any) {
607 |       console.error("Error querying records:", error);
608 |       return {
609 |         content: [
610 |           {
611 |             type: "text",
612 |             text: `Failed to query records: ${error.message}`,
613 |           },
614 |         ],
615 |       };
616 |     }
617 |   }
618 | );
619 | 
620 | // PowerPlatform MCP Prompts
621 | server.tool(
622 |   "use-powerplatform-prompt",
623 |   "Use a predefined prompt template for PowerPlatform entities",
624 |   {
625 |     promptType: z.enum([
626 |       "ENTITY_OVERVIEW", 
627 |       "ATTRIBUTE_DETAILS", 
628 |       "QUERY_TEMPLATE", 
629 |       "RELATIONSHIP_MAP"
630 |     ]).describe("The type of prompt template to use"),
631 |     entityName: z.string().describe("The logical name of the entity"),
632 |     attributeName: z.string().optional().describe("The logical name of the attribute (required for ATTRIBUTE_DETAILS prompt)"),
633 |   },
634 |   async ({ promptType, entityName, attributeName }) => {
635 |     try {
636 |       // Get or initialize PowerPlatformService
637 |       const service = getPowerPlatformService();
638 |       
639 |       let promptContent = "";
640 |       let replacements: Record<string, string> = {};
641 |       
642 |       switch (promptType) {
643 |         case "ENTITY_OVERVIEW": {
644 |           // Get entity metadata and key attributes
645 |           const [metadata, attributes] = await Promise.all([
646 |             service.getEntityMetadata(entityName),
647 |             service.getEntityAttributes(entityName)
648 |           ]);
649 |           
650 |           // Format entity details
651 |           const entityDetails = `- Display Name: ${metadata.DisplayName?.UserLocalizedLabel?.Label || entityName}\n` +
652 |             `- Schema Name: ${metadata.SchemaName}\n` +
653 |             `- Description: ${metadata.Description?.UserLocalizedLabel?.Label || 'No description'}\n` +
654 |             `- Primary Key: ${metadata.PrimaryIdAttribute}\n` +
655 |             `- Primary Name: ${metadata.PrimaryNameAttribute}`;
656 |             
657 |           // Get key attributes
658 |           const keyAttributes = attributes.value
659 |             //.slice(0, 10) // Limit to first 10 important attributes
660 |             .map((attr: any) => {
661 |                 const attrType = attr["@odata.type"] || attr.odata?.type || "Unknown type";
662 |                 return `- ${attr.LogicalName}: ${attrType}`;
663 |               })
664 |             .join('\n');
665 |             
666 |           // Get relationships summary
667 |           const relationships = await service.getEntityRelationships(entityName);
668 |           const oneToManyCount = relationships.oneToMany.value.length;
669 |           const manyToManyCount = relationships.manyToMany.value.length;
670 |           
671 |           const relationshipsSummary = `- One-to-Many Relationships: ${oneToManyCount}\n` +
672 |                                       `- Many-to-Many Relationships: ${manyToManyCount}`;
673 |           
674 |           promptContent = powerPlatformPrompts.ENTITY_OVERVIEW(entityName);
675 |           replacements = {
676 |             '{{entity_details}}': entityDetails,
677 |             '{{key_attributes}}': keyAttributes,
678 |             '{{relationships}}': relationshipsSummary
679 |           };
680 |           break;
681 |         }
682 |         
683 |         case "ATTRIBUTE_DETAILS": {
684 |           if (!attributeName) {
685 |             throw new Error("attributeName is required for ATTRIBUTE_DETAILS prompt");
686 |           }
687 |           
688 |           // Get attribute details
689 |           const attribute = await service.getEntityAttribute(entityName, attributeName);
690 |           
691 |           // Format attribute details
692 |           const attrDetails = `- Display Name: ${attribute.DisplayName?.UserLocalizedLabel?.Label || attributeName}\n` +
693 |             `- Description: ${attribute.Description?.UserLocalizedLabel?.Label || 'No description'}\n` +
694 |             `- Type: ${attribute.AttributeType}\n` +
695 |             `- Format: ${attribute.Format || 'N/A'}\n` +
696 |             `- Is Required: ${attribute.RequiredLevel?.Value || 'No'}\n` +
697 |             `- Is Searchable: ${attribute.IsValidForAdvancedFind || false}`;
698 |             
699 |           promptContent = powerPlatformPrompts.ATTRIBUTE_DETAILS(entityName, attributeName);
700 |           replacements = {
701 |             '{{attribute_details}}': attrDetails,
702 |             '{{data_type}}': attribute.AttributeType,
703 |             '{{required}}': attribute.RequiredLevel?.Value || 'No',
704 |             '{{max_length}}': attribute.MaxLength || 'N/A'
705 |           };
706 |           break;
707 |         }
708 |         
709 |         case "QUERY_TEMPLATE": {
710 |           // Get entity metadata to determine plural name
711 |           const metadata = await service.getEntityMetadata(entityName);
712 |           const entityNamePlural = metadata.EntitySetName;
713 |           
714 |           // Get a few important fields for the select example
715 |           const attributes = await service.getEntityAttributes(entityName);
716 |           const selectFields = attributes.value
717 |             .slice(0, 5) // Just take first 5 for example
718 |             .map((attr: any) => attr.LogicalName)
719 |             .join(',');
720 |             
721 |           promptContent = powerPlatformPrompts.QUERY_TEMPLATE(entityNamePlural);
722 |           replacements = {
723 |             '{{selected_fields}}': selectFields,
724 |             '{{filter_conditions}}': `${metadata.PrimaryNameAttribute} eq 'Example'`,
725 |             '{{order_by}}': `${metadata.PrimaryNameAttribute} asc`,
726 |             '{{max_records}}': '50'
727 |           };
728 |           break;
729 |         }
730 |         
731 |         case "RELATIONSHIP_MAP": {
732 |           // Get relationships
733 |           const relationships = await service.getEntityRelationships(entityName);
734 |           
735 |           // Format one-to-many relationships where this entity is primary
736 |           const oneToManyPrimary = relationships.oneToMany.value
737 |             .filter((rel: any) => rel.ReferencingEntity !== entityName)
738 |             //.slice(0, 10) // Limit to 10 for readability
739 |             .map((rel: any) => `- ${rel.SchemaName}: ${entityName} (1) → ${rel.ReferencingEntity} (N)`)
740 |             .join('\n');
741 |             
742 |           // Format one-to-many relationships where this entity is related
743 |           const oneToManyRelated = relationships.oneToMany.value
744 |             .filter((rel: any) => rel.ReferencingEntity === entityName)
745 |             //.slice(0, 10) // Limit to 10 for readability
746 |             .map((rel: any) => `- ${rel.SchemaName}: ${rel.ReferencedEntity} (1) → ${entityName} (N)`)
747 |             .join('\n');
748 |             
749 |           // Format many-to-many relationships
750 |           const manyToMany = relationships.manyToMany.value
751 |             //.slice(0, 10) // Limit to 10 for readability
752 |             .map((rel: any) => {
753 |               const otherEntity = rel.Entity1LogicalName === entityName ? rel.Entity2LogicalName : rel.Entity1LogicalName;
754 |               return `- ${rel.SchemaName}: ${entityName} (N) ↔ ${otherEntity} (N)`;
755 |             })
756 |             .join('\n');
757 |           
758 |           promptContent = powerPlatformPrompts.RELATIONSHIP_MAP(entityName);
759 |           replacements = {
760 |             '{{one_to_many_primary}}': oneToManyPrimary || 'None found',
761 |             '{{one_to_many_related}}': oneToManyRelated || 'None found',
762 |             '{{many_to_many}}': manyToMany || 'None found'
763 |           };
764 |           break;
765 |         }
766 |       }
767 |       
768 |       // Replace all placeholders in the template
769 |       for (const [placeholder, value] of Object.entries(replacements)) {
770 |         promptContent = promptContent.replace(placeholder, value);
771 |       }
772 |       
773 |       return {
774 |         content: [
775 |           {
776 |             type: "text",
777 |             text: promptContent,
778 |           },
779 |         ],
780 |       };
781 |     } catch (error: any) {
782 |       console.error("Error using PowerPlatform prompt:", error);
783 |       return {
784 |         content: [
785 |           {
786 |             type: "text",
787 |             text: `Failed to use PowerPlatform prompt: ${error.message}`,
788 |           },
789 |         ],
790 |       };
791 |     }
792 |   }
793 | );
794 | 
795 | async function main() {
796 |   const transport = new StdioServerTransport();
797 |   await server.connect(transport);
798 |   console.error("Initializing PowerPlatform MCP Server...");
799 | }
800 | 
801 | main().catch((error) => {
802 |   console.error("Fatal error in main():", error);
803 |   process.exit(1);
804 | });
```