UNPKG

powerplatform-mcp

Version:

PowerPlatform Model Context Protocol server

242 lines (241 loc) 9.76 kB
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}`); } } }