@dav3/mcp-dynamics365-server
Version:
Generic MCP server for Dynamics 365 CRM operations (AI-generated side project with limited maintenance)
394 lines (393 loc) • 17 kB
JavaScript
import axios from "axios";
import { D365AuthService } from "./auth.js";
export class D365ApiClient {
authService;
baseUrl;
constructor(config) {
this.authService = new D365AuthService(config);
this.baseUrl = config.baseUrl;
}
/**
* Get metadata for a specific entity
*/
async getEntityMetadata(entityName, includeAttributes = true) {
try {
const headers = await this.authService.getAuthHeaders();
if (includeAttributes) {
// Get full entity definition with attributes
const url = `${this.baseUrl}/api/data/v9.2/EntityDefinitions?$filter=LogicalName eq '${entityName}'&$expand=Attributes($select=LogicalName,DisplayName,AttributeType,IsPrimaryId,RequiredLevel,Description)`;
const response = await axios.get(url, { headers });
const entityDefs = response.data.value;
if (!entityDefs || entityDefs.length === 0) {
throw new Error(`Entity '${entityName}' not found`);
}
const entityDef = entityDefs[0];
// Extract attributes from the EntityDefinition
const attributes = entityDef.Attributes?.map((attr) => ({
LogicalName: attr.LogicalName || "",
DisplayName: attr.DisplayName?.UserLocalizedLabel?.Label ||
attr.LogicalName ||
"",
AttributeType: attr.AttributeType || "Unknown",
IsPrimaryKey: attr.IsPrimaryId || false,
IsRequired: attr.RequiredLevel?.Value === 2, // ApplicationRequired = 2
MaxLength: undefined, // Will be populated separately for string attributes if needed
Description: attr.Description?.UserLocalizedLabel?.Label || undefined,
})) || [];
const metadata = {
EntityType: entityDef.LogicalName || entityName,
EntitySetName: entityDef.LogicalCollectionName || entityName,
PrimaryKey: attributes.find((attr) => attr.IsPrimaryKey)?.LogicalName || "id",
DisplayName: entityDef.DisplayName?.UserLocalizedLabel?.Label || entityName,
Description: entityDef.Description?.UserLocalizedLabel?.Label ||
`Entity definition for ${entityName}`,
Attributes: attributes,
};
return {
success: true,
data: metadata,
statusCode: response.status,
};
}
else {
// Get basic entity definition without attributes
const url = `${this.baseUrl}/api/data/v9.2/EntityDefinitions?$filter=LogicalName eq '${entityName}'&$select=LogicalName,LogicalCollectionName,DisplayName,Description,PrimaryIdAttribute`;
const response = await axios.get(url, { headers });
const entityDefs = response.data.value;
if (!entityDefs || entityDefs.length === 0) {
throw new Error(`Entity '${entityName}' not found`);
}
const entityDef = entityDefs[0];
const metadata = {
EntityType: entityDef.LogicalName || entityName,
EntitySetName: entityDef.LogicalCollectionName || entityName,
PrimaryKey: entityDef.PrimaryIdAttribute || "id",
DisplayName: entityDef.DisplayName?.UserLocalizedLabel?.Label || entityName,
Description: entityDef.Description?.UserLocalizedLabel?.Label ||
`Entity definition for ${entityName}`,
Attributes: [],
};
return {
success: true,
data: metadata,
statusCode: response.status,
};
}
}
catch (error) {
return this.handleError(error, `Failed to get metadata for entity: ${entityName}`);
}
}
/**
* Get detailed metadata for a specific attribute of an entity
*/
async getAttributeMetadata(entityName, attributeName) {
try {
const headers = await this.authService.getAuthHeaders();
// Get detailed attribute definition
let url = `${this.baseUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes(LogicalName='${attributeName}')`;
// First, get basic attribute info to determine type
let response = await axios.get(url, { headers });
let attributeDef = response.data;
if (!attributeDef) {
throw new Error(`Attribute '${attributeName}' not found on entity '${entityName}'`);
}
// If it's a picklist/status attribute, get it with OptionSet expansion using casting
if (attributeDef.AttributeType === "Picklist" ||
attributeDef.AttributeType === "State" ||
attributeDef.AttributeType === "Status") {
const castType = attributeDef.AttributeType === "Picklist"
? "Microsoft.Dynamics.CRM.PicklistAttributeMetadata"
: attributeDef.AttributeType === "State"
? "Microsoft.Dynamics.CRM.StateAttributeMetadata"
: "Microsoft.Dynamics.CRM.StatusAttributeMetadata";
url = `${this.baseUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityName}')/Attributes(LogicalName='${attributeName}')/${castType}?$expand=OptionSet`;
response = await axios.get(url, { headers });
attributeDef = response.data;
}
// Build detailed metadata
const detailedMetadata = {
LogicalName: attributeDef.LogicalName || attributeName,
DisplayName: attributeDef.DisplayName?.UserLocalizedLabel?.Label ||
attributeDef.LogicalName ||
attributeName,
AttributeType: attributeDef.AttributeType || "Unknown",
IsPrimaryKey: attributeDef.IsPrimaryId || false,
IsRequired: attributeDef.RequiredLevel?.Value === 2, // ApplicationRequired = 2
Description: attributeDef.Description?.UserLocalizedLabel?.Label || undefined,
SchemaName: attributeDef.SchemaName,
EntityLogicalName: attributeDef.EntityLogicalName,
IsValidForAdvancedFind: attributeDef.IsValidForAdvancedFind,
IsValidForCreate: attributeDef.IsValidForCreate,
IsValidForRead: attributeDef.IsValidForRead,
IsValidForUpdate: attributeDef.IsValidForUpdate,
CanBeSecuredForCreate: attributeDef.CanBeSecuredForCreate,
CanBeSecuredForRead: attributeDef.CanBeSecuredForRead,
CanBeSecuredForUpdate: attributeDef.CanBeSecuredForUpdate,
IsSecured: attributeDef.IsSecured,
MaxLength: attributeDef.MaxLength,
MinValue: attributeDef.MinValue,
MaxValue: attributeDef.MaxValue,
Precision: attributeDef.Precision,
Format: attributeDef.Format,
DateTimeBehavior: attributeDef.DateTimeBehavior?.Value,
ImeMode: attributeDef.ImeMode?.Value,
};
// Handle Picklist/OptionSet attributes - get the options
if (attributeDef.AttributeType === "Picklist" ||
attributeDef.AttributeType === "State" ||
attributeDef.AttributeType === "Status") {
if (attributeDef.OptionSet && attributeDef.OptionSet.Options) {
detailedMetadata.Options = attributeDef.OptionSet.Options.map((option) => ({
Value: option.Value,
Label: option.Label?.UserLocalizedLabel?.Label ||
`Option ${option.Value}`,
Description: option.Description?.UserLocalizedLabel?.Label,
Color: option.Color,
}));
}
detailedMetadata.DefaultFormValue = attributeDef.DefaultFormValue;
}
// Handle Lookup attributes - get target entities
if (attributeDef.AttributeType === "Lookup" ||
attributeDef.AttributeType === "Customer" ||
attributeDef.AttributeType === "Owner") {
if (attributeDef.Targets) {
detailedMetadata.Targets = attributeDef.Targets;
}
}
return {
success: true,
data: detailedMetadata,
statusCode: response.status,
};
}
catch (error) {
return this.handleError(error, `Failed to get metadata for attribute '${attributeName}' on entity '${entityName}'`);
}
}
/**
* Get all entity sets (list of available entities)
*/
async getEntitySets() {
try {
const headers = await this.authService.getAuthHeaders();
// Query EntityDefinitions to get all entities
const url = `${this.baseUrl}/api/data/v9.2/EntityDefinitions?$select=LogicalName,LogicalCollectionName,DisplayName&$filter=IsValidForAdvancedFind eq true and IsCustomizable/Value eq true`;
const response = await axios.get(url, { headers });
// Extract entity set names from EntityDefinitions
const entitySets = response.data.value
?.map((entity) => entity.LogicalCollectionName || entity.LogicalName)
.filter((name) => name) || [];
return {
success: true,
data: entitySets,
statusCode: response.status,
};
}
catch (error) {
// If the above fails, fall back to common entities
console.log("EntityDefinitions query failed, falling back to common entities");
const commonEntities = [
"accounts",
"contacts",
"leads",
"opportunities",
"cases",
"tasks",
"appointments",
"emails",
"phonecalls",
"activities",
"systemusers",
"teams",
"businessunits",
"roles",
"subjects",
"products",
"pricelists",
"quotes",
"orders",
"invoices",
];
return {
success: true,
data: commonEntities,
statusCode: 200,
};
}
}
/**
* Query entities with flexible options
*/
async queryEntities(entitySet, options = {}) {
try {
const headers = await this.authService.getAuthHeaders();
const queryParams = this.buildQueryParams(options);
const url = `${this.baseUrl}/api/data/v9.2/${entitySet}${queryParams}`;
const response = await axios.get(url, { headers });
return {
success: true,
data: response.data,
statusCode: response.status,
};
}
catch (error) {
return this.handleError(error, `Failed to query entities from: ${entitySet}`);
}
}
/**
* Get a specific entity by ID
*/
async getEntity(entitySet, id, select) {
try {
const headers = await this.authService.getAuthHeaders();
const selectParam = select ? `?$select=${select.join(",")}` : "";
const url = `${this.baseUrl}/api/data/v9.2/${entitySet}(${id})${selectParam}`;
const response = await axios.get(url, { headers });
return {
success: true,
data: response.data,
statusCode: response.status,
};
}
catch (error) {
return this.handleError(error, `Failed to get entity ${id} from: ${entitySet}`);
}
}
/**
* Create a new entity
*/
async createEntity(entitySet, data) {
try {
const headers = await this.authService.getAuthHeaders();
const url = `${this.baseUrl}/api/data/v9.2/${entitySet}`;
const response = await axios.post(url, data, { headers });
return {
success: true,
data: response.data,
statusCode: response.status,
};
}
catch (error) {
return this.handleError(error, `Failed to create entity in: ${entitySet}`);
}
}
/**
* Update an existing entity
*/
async updateEntity(entitySet, id, data) {
try {
const headers = await this.authService.getAuthHeaders();
const url = `${this.baseUrl}/api/data/v9.2/${entitySet}(${id})`;
const response = await axios.patch(url, data, { headers });
return {
success: true,
data: response.data || { updated: true },
statusCode: response.status,
};
}
catch (error) {
return this.handleError(error, `Failed to update entity ${id} in: ${entitySet}`);
}
}
/**
* Delete an entity
*/
async deleteEntity(entitySet, id) {
try {
const headers = await this.authService.getAuthHeaders();
const url = `${this.baseUrl}/api/data/v9.2/${entitySet}(${id})`;
const response = await axios.delete(url, { headers });
return {
success: true,
data: { deleted: true },
statusCode: response.status,
};
}
catch (error) {
return this.handleError(error, `Failed to delete entity ${id} from: ${entitySet}`);
}
}
/**
* Execute a custom OData query
*/
async executeCustomQuery(odataQuery) {
try {
const headers = await this.authService.getAuthHeaders();
const url = `${this.baseUrl}/api/data/v9.2/${odataQuery}`;
const response = await axios.get(url, { headers });
return {
success: true,
data: response.data,
statusCode: response.status,
};
}
catch (error) {
return this.handleError(error, `Failed to execute custom query: ${odataQuery}`);
}
}
/**
* Execute a function or action
*/
async executeFunction(functionName, parameters = {}, method = "GET") {
try {
const headers = await this.authService.getAuthHeaders();
const url = `${this.baseUrl}/api/data/v9.2/${functionName}`;
let response;
if (method === "GET") {
const params = new URLSearchParams(parameters).toString();
const fullUrl = params ? `${url}?${params}` : url;
response = await axios.get(fullUrl, { headers });
}
else {
response = await axios.request({
method,
url,
headers,
data: parameters,
});
}
return {
success: true,
data: response.data,
statusCode: response.status,
};
}
catch (error) {
return this.handleError(error, `Failed to execute function: ${functionName}`);
}
}
buildQueryParams(options) {
const params = [];
if (options.select) {
params.push(`$select=${options.select.join(",")}`);
}
if (options.filter) {
params.push(`$filter=${encodeURIComponent(options.filter)}`);
}
if (options.orderby) {
params.push(`$orderby=${encodeURIComponent(options.orderby)}`);
}
if (options.top) {
params.push(`$top=${options.top}`);
}
if (options.skip) {
params.push(`$skip=${options.skip}`);
}
if (options.expand) {
params.push(`$expand=${encodeURIComponent(options.expand)}`);
}
return params.length > 0 ? `?${params.join("&")}` : "";
}
handleError(error, message) {
const errorMessage = error.response?.data?.error?.message || error.message || "Unknown error";
const statusCode = error.response?.status || 500;
console.error(`${message}:`, errorMessage);
return {
success: false,
error: `${message}: ${errorMessage}`,
statusCode,
};
}
}