powerplatform-mcp
Version:
PowerPlatform Model Context Protocol server
242 lines (241 loc) • 9.76 kB
JavaScript
import { ConfidentialClientApplication } from '@azure/msal-node';
import axios from 'axios';
/**
* Base client for PowerPlatform API access.
* Handles authentication and generic HTTP requests.
* Service classes should depend on this client via constructor injection.
*/
export class PowerPlatformClient {
config;
msalClient;
accessToken = null;
tokenExpirationTime = 0;
managementAccessToken = null;
managementTokenExpirationTime = 0;
constructor(config) {
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 the organization URL
*/
get organizationUrl() {
return this.config.organizationUrl;
}
/**
* Get an access token for the PowerPlatform API
*/
async getAccessToken() {
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');
}
}
/**
* Get an access token for the Flow Management API (api.flow.microsoft.com).
* Uses a different scope than the Dataverse token.
*/
async getManagementToken() {
const currentTime = Date.now();
if (this.managementAccessToken && this.managementTokenExpirationTime > currentTime) {
return this.managementAccessToken;
}
try {
const result = await this.msalClient.acquireTokenByClientCredential({
scopes: ['https://service.flow.microsoft.com/.default'],
});
if (!result || !result.accessToken) {
throw new Error('Failed to acquire management access token');
}
this.managementAccessToken = result.accessToken;
if (result.expiresOn) {
this.managementTokenExpirationTime = result.expiresOn.getTime() - (5 * 60 * 1000);
}
return this.managementAccessToken;
}
catch (error) {
console.error('Error acquiring management access token:', error);
throw new Error('Management API authentication failed');
}
}
/**
* Make an authenticated GET request to the PowerPlatform API
* @param endpoint The API endpoint (relative to organization URL)
* @param extraHeaders Optional extra headers (e.g. `Prefer: odata.maxpagesize=500` for pagination)
*/
async get(endpoint, extraHeaders) {
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',
...extraHeaders,
}
});
return response.data;
}
catch (error) {
const detail = error?.response?.data?.error?.message ?? error?.message ?? error;
console.error('PowerPlatform API request failed:', detail);
throw new Error(`PowerPlatform API request failed: ${detail}`);
}
}
/**
* Make an authenticated POST request to the PowerPlatform API.
* Handles Dataverse 204 responses by extracting the record ID from
* the OData-EntityId header when available.
* @param endpoint The API endpoint (relative to organization URL)
* @param data The request body
*/
async post(endpoint, data, extraHeaders) {
try {
const token = await this.getAccessToken();
const response = await axios({
method: 'POST',
url: `${this.config.organizationUrl}/${endpoint}`,
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0',
...extraHeaders,
},
data,
validateStatus: (status) => status >= 200 && status < 300,
});
if (response.status === 204) {
const entityIdHeader = response.headers['odata-entityid'];
if (entityIdHeader) {
const match = entityIdHeader.match(/\(([^)]+)\)/);
return { entityId: match ? match[1] : entityIdHeader };
}
return undefined;
}
return response.data;
}
catch (error) {
const detail = error?.response?.data?.error?.message ?? error?.message ?? error;
console.error('PowerPlatform API POST request failed:', detail);
throw new Error(`PowerPlatform API POST request failed: ${detail}`);
}
}
/**
* Make an authenticated PATCH request to the PowerPlatform API.
* Used for update and upsert operations. Dataverse returns 204 No Content.
* @param endpoint The API endpoint (relative to organization URL)
* @param data The request body
* @param extraHeaders Additional headers (e.g. MSCRM.SolutionUniqueName)
*/
async patch(endpoint, data, extraHeaders) {
try {
const token = await this.getAccessToken();
await axios({
method: 'PATCH',
url: `${this.config.organizationUrl}/${endpoint}`,
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0',
...extraHeaders,
},
data
});
}
catch (error) {
const detail = error?.response?.data?.error?.message ?? error?.message ?? error;
console.error('PowerPlatform API PATCH request failed:', detail);
throw new Error(`PowerPlatform API PATCH request failed: ${detail}`);
}
}
/**
* Make an authenticated PUT request to the PowerPlatform API.
* Required for metadata updates (EntityDefinitions etc.) — Dataverse treats metadata
* PUT as a full-replace UpdateEntityRequest; partial PATCH is not supported.
* @param endpoint The API endpoint (relative to organization URL)
* @param data The full entity body
* @param extraHeaders Additional headers (e.g. MSCRM.MergeLabels, MSCRM.SolutionUniqueName)
*/
async put(endpoint, data, extraHeaders) {
try {
const token = await this.getAccessToken();
await axios({
method: 'PUT',
url: `${this.config.organizationUrl}/${endpoint}`,
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0',
...extraHeaders,
},
data
});
}
catch (error) {
const detail = error?.response?.data?.error?.message ?? error?.message ?? error;
console.error('PowerPlatform API PUT request failed:', detail);
throw new Error(`PowerPlatform API PUT request failed: ${detail}`);
}
}
/**
* Make an authenticated DELETE request to the PowerPlatform API.
* Dataverse returns 204 No Content.
* @param endpoint The API endpoint (relative to organization URL)
*/
async delete(endpoint) {
try {
const token = await this.getAccessToken();
await axios({
method: 'DELETE',
url: `${this.config.organizationUrl}/${endpoint}`,
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0'
}
});
}
catch (error) {
const detail = error?.response?.data?.error?.message ?? error?.message ?? error;
console.error('PowerPlatform API DELETE request failed:', detail);
throw new Error(`PowerPlatform API DELETE request failed: ${detail}`);
}
}
}