Skip to main content
Glama

Dataverse MCP Server

by mwhesse
dataverse-client.ts15.3 kB
import axios, { AxiosInstance, AxiosResponse } from 'axios'; import * as fs from 'fs'; import * as path from 'path'; export interface DataverseConfig { dataverseUrl: string; clientId: string; clientSecret: string; tenantId: string; } export interface AuthToken { access_token: string; token_type: string; expires_in: number; expires_at: number; } export interface DataverseError { error: { code: string; message: string; innererror?: { message: string; type: string; stacktrace: string; }; }; } export interface SolutionContext { solutionUniqueName: string; solutionDisplayName?: string; publisherUniqueName?: string; publisherDisplayName?: string; customizationPrefix?: string; lastUpdated: string; } export class DataverseClient { private config: DataverseConfig; private httpClient: AxiosInstance; private authToken: AuthToken | null = null; private solutionUniqueName: string | null = null; private solutionContext: SolutionContext | null = null; private contextFilePath: string; constructor(config: DataverseConfig) { this.config = config; this.contextFilePath = path.join(process.cwd(), '.dataverse-mcp'); // Load persisted solution context on startup this.loadSolutionContext(); this.httpClient = axios.create({ baseURL: `${config.dataverseUrl}/api/data/v9.2/`, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'OData-MaxVersion': '4.0', 'OData-Version': '4.0' } }); // Add request interceptor to handle authentication this.httpClient.interceptors.request.use(async (config) => { await this.ensureAuthenticated(); if (this.authToken) { config.headers.Authorization = `Bearer ${this.authToken.access_token}`; } return config; }); // Add response interceptor to handle errors this.httpClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.data?.error) { const dataverseError = error.response.data as DataverseError; throw new Error(`Dataverse API Error: ${dataverseError.error.message} (Code: ${dataverseError.error.code})`); } throw error; } ); } private async authenticate(): Promise<AuthToken> { const tokenUrl = `https://login.microsoftonline.com/${this.config.tenantId}/oauth2/v2.0/token`; const params = new URLSearchParams(); params.append('grant_type', 'client_credentials'); params.append('client_id', this.config.clientId); params.append('client_secret', this.config.clientSecret); params.append('scope', `${this.config.dataverseUrl}/.default`); try { const response = await axios.post(tokenUrl, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); const token: AuthToken = { ...response.data, expires_at: Date.now() + (response.data.expires_in * 1000) - 60000 // Subtract 1 minute for safety }; return token; } catch (error) { throw new Error(`Authentication failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } private async ensureAuthenticated(): Promise<void> { if (!this.authToken || Date.now() >= this.authToken.expires_at) { this.authToken = await this.authenticate(); } } // Solution context persistence methods private loadSolutionContext(): void { try { if (fs.existsSync(this.contextFilePath)) { const contextData = fs.readFileSync(this.contextFilePath, 'utf8'); this.solutionContext = JSON.parse(contextData); this.solutionUniqueName = this.solutionContext?.solutionUniqueName || null; if (this.solutionContext) { console.log(`Loaded solution context: ${this.solutionContext.solutionUniqueName} (${this.solutionContext.solutionDisplayName || 'Unknown'})`); } } } catch (error) { console.warn('Failed to load solution context from .dataverse-mcp file:', error instanceof Error ? error.message : 'Unknown error'); // Reset context on error this.solutionContext = null; this.solutionUniqueName = null; } } private saveSolutionContext(): void { try { if (this.solutionContext) { fs.writeFileSync(this.contextFilePath, JSON.stringify(this.solutionContext, null, 2), 'utf8'); } else { // Remove file when context is cleared if (fs.existsSync(this.contextFilePath)) { fs.unlinkSync(this.contextFilePath); } } } catch (error) { console.warn('Failed to save solution context to .dataverse-mcp file:', error instanceof Error ? error.message : 'Unknown error'); } } // Enhanced solution context methods async setSolutionContext(solutionUniqueName: string): Promise<void> { try { // Fetch solution details to populate context const result = await this.get( `solutions?$filter=uniquename eq '${solutionUniqueName}'&$expand=publisherid($select=uniquename,friendlyname,customizationprefix)&$select=uniquename,friendlyname` ); if (!result.value || result.value.length === 0) { throw new Error(`Solution '${solutionUniqueName}' not found`); } const solution = result.value[0]; const publisher = solution.publisherid; this.solutionContext = { solutionUniqueName: solution.uniquename, solutionDisplayName: solution.friendlyname, publisherUniqueName: publisher?.uniquename, publisherDisplayName: publisher?.friendlyname, customizationPrefix: publisher?.customizationprefix, lastUpdated: new Date().toISOString() }; this.solutionUniqueName = solutionUniqueName; this.saveSolutionContext(); } catch (error) { throw new Error(`Failed to set solution context: ${error instanceof Error ? error.message : 'Unknown error'}`); } } getSolutionContext(): SolutionContext | null { return this.solutionContext; } getSolutionUniqueName(): string | null { return this.solutionUniqueName; } clearSolutionContext(): void { this.solutionUniqueName = null; this.solutionContext = null; this.saveSolutionContext(); } // Helper method to get headers with solution context private getMetadataHeaders(): Record<string, string> { const headers: Record<string, string> = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'OData-MaxVersion': '4.0', 'OData-Version': '4.0' }; if (this.solutionUniqueName) { headers['MSCRM.SolutionUniqueName'] = this.solutionUniqueName; } return headers; } // Helper method to get the customization prefix from the current solution context getCustomizationPrefix(): string | null { if (!this.solutionContext) { return null; } return this.solutionContext.customizationPrefix || null; } // Async method to refresh and get customization prefix (for backward compatibility) async getCustomizationPrefixAsync(): Promise<string> { if (!this.solutionUniqueName) { throw new Error('No solution context is set. Please set a solution context using set_solution_context tool to get the customization prefix.'); } // If we have cached prefix, return it if (this.solutionContext?.customizationPrefix) { return this.solutionContext.customizationPrefix; } // Otherwise fetch it try { const result = await this.get( `solutions?$filter=uniquename eq '${this.solutionUniqueName}'&$expand=publisherid($select=customizationprefix)` ); if (!result.value || result.value.length === 0) { throw new Error(`Solution '${this.solutionUniqueName}' not found`); } const prefix = result.value[0].publisherid?.customizationprefix; if (!prefix) { throw new Error(`No customization prefix found for solution '${this.solutionUniqueName}'`); } return prefix; } catch (error) { throw new Error(`Failed to get customization prefix: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Generic HTTP methods async get<T = any>(endpoint: string, params?: Record<string, any>): Promise<T> { const response: AxiosResponse<T> = await this.httpClient.get(endpoint, { params }); return response.data; } async post<T = any>(endpoint: string, data?: any): Promise<T> { const response: AxiosResponse<T> = await this.httpClient.post(endpoint, data); return response.data; } async patch<T = any>(endpoint: string, data?: any): Promise<T> { const response: AxiosResponse<T> = await this.httpClient.patch(endpoint, data); return response.data; } async put<T = any>(endpoint: string, data?: any): Promise<T> { const response: AxiosResponse<T> = await this.httpClient.put(endpoint, data); return response.data; } async delete(endpoint: string): Promise<void> { await this.httpClient.delete(endpoint); } // Metadata-specific methods async getMetadata<T = any>(endpoint: string, params?: Record<string, any>): Promise<T> { const metadataClient = axios.create({ baseURL: `${this.config.dataverseUrl}/api/data/v9.2/`, headers: this.getMetadataHeaders() }); // Add error interceptor metadataClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.data?.error) { const dataverseError = error.response.data as DataverseError; throw new Error(`Dataverse API Error: ${dataverseError.error.message} (Code: ${dataverseError.error.code})`); } throw error; } ); await this.ensureAuthenticated(); if (this.authToken) { metadataClient.defaults.headers.Authorization = `Bearer ${this.authToken.access_token}`; } const response: AxiosResponse<T> = await metadataClient.get(endpoint, { params }); return response.data; } async postMetadata<T = any>(endpoint: string, data?: any): Promise<T> { const metadataClient = axios.create({ baseURL: `${this.config.dataverseUrl}/api/data/v9.2/`, headers: this.getMetadataHeaders() }); // Add error interceptor metadataClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.data?.error) { const dataverseError = error.response.data as DataverseError; throw new Error(`Dataverse API Error: ${dataverseError.error.message} (Code: ${dataverseError.error.code})`); } throw error; } ); await this.ensureAuthenticated(); if (this.authToken) { metadataClient.defaults.headers.Authorization = `Bearer ${this.authToken.access_token}`; } const response: AxiosResponse<T> = await metadataClient.post(endpoint, data); return response.data; } async patchMetadata<T = any>(endpoint: string, data?: any): Promise<T> { const metadataClient = axios.create({ baseURL: `${this.config.dataverseUrl}/api/data/v9.2/`, headers: this.getMetadataHeaders() }); // Add error interceptor metadataClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.data?.error) { const dataverseError = error.response.data as DataverseError; throw new Error(`Dataverse API Error: ${dataverseError.error.message} (Code: ${dataverseError.error.code})`); } throw error; } ); await this.ensureAuthenticated(); if (this.authToken) { metadataClient.defaults.headers.Authorization = `Bearer ${this.authToken.access_token}`; } const response: AxiosResponse<T> = await metadataClient.patch(endpoint, data); return response.data; } async putMetadata<T = any>(endpoint: string, data?: any, additionalHeaders?: Record<string, string>): Promise<T> { const headers = { ...this.getMetadataHeaders(), ...additionalHeaders }; const metadataClient = axios.create({ baseURL: `${this.config.dataverseUrl}/api/data/v9.2/`, headers }); // Add error interceptor metadataClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.data?.error) { const dataverseError = error.response.data as DataverseError; throw new Error(`Dataverse API Error: ${dataverseError.error.message} (Code: ${dataverseError.error.code})`); } throw error; } ); await this.ensureAuthenticated(); if (this.authToken) { metadataClient.defaults.headers.Authorization = `Bearer ${this.authToken.access_token}`; } const response: AxiosResponse<T> = await metadataClient.put(endpoint, data); return response.data; } async deleteMetadata(endpoint: string): Promise<void> { const metadataClient = axios.create({ baseURL: `${this.config.dataverseUrl}/api/data/v9.2/`, headers: this.getMetadataHeaders() }); // Add error interceptor metadataClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.data?.error) { const dataverseError = error.response.data as DataverseError; throw new Error(`Dataverse API Error: ${dataverseError.error.message} (Code: ${dataverseError.error.code})`); } throw error; } ); await this.ensureAuthenticated(); if (this.authToken) { metadataClient.defaults.headers.Authorization = `Bearer ${this.authToken.access_token}`; } await metadataClient.delete(endpoint); } // Action-specific method for calling Dataverse actions async callAction<T = any>(actionName: string, data?: any): Promise<T> { const actionClient = axios.create({ baseURL: `${this.config.dataverseUrl}/api/data/v9.2/`, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'OData-MaxVersion': '4.0', 'OData-Version': '4.0' } }); // Add error interceptor actionClient.interceptors.response.use( (response) => response, (error) => { if (error.response?.data?.error) { const dataverseError = error.response.data as DataverseError; throw new Error(`Dataverse API Error: ${dataverseError.error.message} (Code: ${dataverseError.error.code})`); } throw error; } ); await this.ensureAuthenticated(); if (this.authToken) { actionClient.defaults.headers.Authorization = `Bearer ${this.authToken.access_token}`; } // Actions should be called with Microsoft.Dynamics.CRM prefix for bound actions // Global actions and option set actions don't need the prefix const globalActions = [ 'PublishXml', 'PublishAllXml', 'ImportSolution', 'ExportSolution', 'InsertOptionValue', 'UpdateOptionValue', 'DeleteOptionValue', 'OrderOption' ]; const actionUrl = globalActions.includes(actionName) ? actionName : `Microsoft.Dynamics.CRM.${actionName}`; const response: AxiosResponse<T> = await actionClient.post(actionUrl, data); return response.data; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mwhesse/dataverse-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server