dumcp
Version:
Power Platform MCP Server
297 lines (253 loc) • 11.4 kB
text/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, method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET', data?: any): Promise<T> {
try {
const token = await this.getAccessToken(); //
const requestOptions = {
method: method,
url: `${this.config.organizationUrl}/${endpoint}`, //
headers: { //
'Authorization': `Bearer ${token}`, //
'Accept': 'application/json', //
'OData-MaxVersion': '4.0', //
'OData-Version': '4.0', //
'Content-Type': 'application/json' // Add Content-Type for POST/PATCH
},
data: data // Include data for POST/PATCH requests
};
const response = await axios(requestOptions); //
return response.data as T; //
} catch (error: any) {
console.error('PowerPlatform API request failed:', error.response?.data || error.message); // More detailed error logging
throw new Error(`PowerPlatform API request failed: ${error.response?.data?.error?.message || error.message}`);
}
}
/**
* 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}`); //
}
/**
* Create a new record in a Power Platform entity
* @param entityNamePlural The plural name of the entity (e.g., 'accounts', 'contacts')
* @param recordData The data for the new record as a JSON object
* @returns The created record's data (often includes the new ID)
*/
async createRecord(entityNamePlural: string, recordData: any): Promise<any> {
return this.makeRequest(`api/data/v9.2/${entityNamePlural}`, 'POST', recordData);
}
/**
* Update an existing record in a Power Platform entity
* @param entityNamePlural The plural name of the entity
* @param recordId The GUID of the record to update
* @param recordData The data to update in the record as a JSON object
*/
async updateRecord(entityNamePlural: string, recordId: string, recordData: any): Promise<void> {
await this.makeRequest(`api/data/v9.2/${entityNamePlural}(${recordId})`, 'PATCH', recordData);
}
/**
* Delete a record from a Power Platform entity by ID
* @param entityNamePlural The plural name of the entity
* @param recordId The GUID of the record to delete
*/
async deleteRecord(entityNamePlural: string, recordId: string): Promise<void> {
await this.makeRequest(`api/data/v9.2/${entityNamePlural}(${recordId})`, 'DELETE');
}
}