UNPKG

andrew-dataverse-mcp

Version:

A Model Context Protocol server for Microsoft Dataverse with comprehensive CRUD operations

242 lines (241 loc) 9.48 kB
import axios from 'axios'; import { DataverseAuth } from '../auth/dataverseAuth.js'; import { getConfig } from '../config/environment.js'; import { createLogger } from '../utils/logger.js'; const logger = createLogger('DataverseService'); export class DataverseService { auth; httpClient; baseUrl; constructor() { logger.info('Initializing Dataverse service...'); this.auth = new DataverseAuth(); this.baseUrl = `${getConfig().dataverse.environmentUrl}/api/data/v9.2`; this.httpClient = axios.create({ baseURL: this.baseUrl, timeout: 30000, // 30 second timeout headers: { 'Content-Type': 'application/json', 'OData-MaxVersion': '4.0', 'OData-Version': '4.0', 'Accept': 'application/json', }, }); // Add request interceptor for authentication this.httpClient.interceptors.request.use(async (config) => { const authHeader = await this.auth.getAuthorizationHeader(); Object.assign(config.headers, authHeader); return config; }); // Add response interceptor for error handling this.httpClient.interceptors.response.use((response) => response, (error) => { this.handleApiError(error); return Promise.reject(error); }); logger.info('Dataverse service initialized successfully'); } handleApiError(error) { if (error.response) { const dataverseError = error.response.data; logger.error(`API Error ${error.response.status}:`, dataverseError.error?.message || error.message); if (dataverseError.error?.innererror) { logger.debug('Inner error details:', dataverseError.error.innererror); } } else if (error.request) { logger.error('Network error - no response received:', error.message); } else { logger.error('Request setup error:', error.message); } } buildQueryString(options) { const params = new URLSearchParams(); if (options.$select) params.append('$select', options.$select); if (options.$filter) params.append('$filter', options.$filter); if (options.$orderby) params.append('$orderby', options.$orderby); if (options.$top) params.append('$top', options.$top.toString()); if (options.$skip) params.append('$skip', options.$skip.toString()); if (options.$expand) params.append('$expand', options.$expand); if (options.$count) params.append('$count', 'true'); const queryString = params.toString(); return queryString ? `?${queryString}` : ''; } /** * Create a new record in the specified table */ async createRecord(request) { try { logger.info(`Creating record in table: ${request.table}`); logger.debug('Record data:', request.data); const response = await this.httpClient.post(`/${request.table}`, request.data, { headers: { 'Prefer': 'return=representation', }, }); // Extract the ID from the OData-EntityId header or response data const entityId = response.headers['odata-entityid'] || response.data[`${request.table.slice(0, -1)}id`] || response.data.id; if (entityId) { // Extract GUID from the full URL if needed const guidMatch = entityId.match(/\(([^)]+)\)/); const recordId = guidMatch ? guidMatch[1] : entityId; logger.info(`Record created successfully with ID: ${recordId}`); return recordId; } throw new Error('Could not extract record ID from response'); } catch (error) { logger.error(`Failed to create record in table ${request.table}:`, error); throw error; } } /** * Read a single record by ID */ async readRecord(request) { try { logger.info(`Reading record ${request.id} from table: ${request.table}`); const queryOptions = {}; if (request.select) queryOptions.$select = request.select; if (request.expand) queryOptions.$expand = request.expand; const queryString = this.buildQueryString(queryOptions); const response = await this.httpClient.get(`/${request.table}(${request.id})${queryString}`); logger.info(`Record retrieved successfully`); return response.data; } catch (error) { logger.error(`Failed to read record ${request.id} from table ${request.table}:`, error); throw error; } } /** * Update an existing record */ async updateRecord(request) { try { logger.info(`Updating record ${request.id} in table: ${request.table}`); logger.debug('Update data:', request.data); await this.httpClient.patch(`/${request.table}(${request.id})`, request.data); logger.info(`Record updated successfully`); } catch (error) { logger.error(`Failed to update record ${request.id} in table ${request.table}:`, error); throw error; } } /** * Delete a record */ async deleteRecord(request) { try { logger.info(`Deleting record ${request.id} from table: ${request.table}`); await this.httpClient.delete(`/${request.table}(${request.id})`); logger.info(`Record deleted successfully`); } catch (error) { logger.error(`Failed to delete record ${request.id} from table ${request.table}:`, error); throw error; } } /** * Query multiple records with OData options */ async queryRecords(request) { try { logger.info(`Querying records from table: ${request.table}`); const queryOptions = { $select: request.select, $filter: request.filter, $orderby: request.orderby, $top: request.top, $skip: request.skip, $expand: request.expand, $count: request.count, }; const queryString = this.buildQueryString(queryOptions); logger.debug(`Query string: ${queryString}`); const response = await this.httpClient.get(`/${request.table}${queryString}`); logger.info(`Query completed successfully. Records returned: ${response.data.value?.length || 0}`); return response.data; } catch (error) { logger.error(`Failed to query records from table ${request.table}:`, error); throw error; } } /** * Associate two records */ async associateRecords(request) { try { logger.info(`Associating record ${request.id} with ${request.relatedId} via ${request.relationshipName}`); const associationData = { '@odata.id': `${this.baseUrl}/${request.relatedTable}(${request.relatedId})`, }; await this.httpClient.post(`/${request.table}(${request.id})/${request.relationshipName}/$ref`, associationData); logger.info(`Records associated successfully`); } catch (error) { logger.error(`Failed to associate records:`, error); throw error; } } /** * Disassociate two records */ async disassociateRecords(request) { try { logger.info(`Disassociating record ${request.id} from ${request.relatedId} via ${request.relationshipName}`); await this.httpClient.delete(`/${request.table}(${request.id})/${request.relationshipName}(${request.relatedId})/$ref`); logger.info(`Records disassociated successfully`); } catch (error) { logger.error(`Failed to disassociate records:`, error); throw error; } } /** * List all available tables */ async listTables() { try { logger.info('Retrieving list of available tables...'); const response = await this.httpClient.get(''); const tables = response.data.value .filter((item) => item.kind === 'EntitySet') .map((item) => item.name); logger.info(`Retrieved ${tables.length} tables`); return tables; } catch (error) { logger.error('Failed to retrieve table list:', error); throw error; } } /** * Get metadata for a specific table */ async getTableMetadata(tableName) { try { logger.info(`Retrieving metadata for table: ${tableName}`); const response = await this.httpClient.get(`$metadata/EntitySets(${tableName})/EntityType`); logger.info(`Metadata retrieved successfully for table: ${tableName}`); return response.data; } catch (error) { logger.error(`Failed to retrieve metadata for table ${tableName}:`, error); throw error; } } }