UNPKG

servicefusion-mcp

Version:

Model Context Protocol server for ServiceFusion API integration - enables AI agents to interact with ServiceFusion customers, jobs, and work orders

163 lines 6.35 kB
import axios from 'axios'; import { CreateCustomerInputSchema, CreateJobInputSchema, UpdateJobInputSchema, DeleteJobInputSchema, } from './types.js'; export class ServiceFusionClient { api; config; token = null; tokenExpiry = null; constructor(config) { this.config = config; this.api = axios.create({ baseURL: config.base_url, headers: { 'Content-Type': 'application/json', }, }); // Add request interceptor for automatic token refresh this.api.interceptors.request.use(async (config) => { await this.ensureValidToken(); if (this.token) { config.headers.Authorization = `Bearer ${this.token.access_token}`; } return config; }); // Add response interceptor for token refresh on 401 this.api.interceptors.response.use((response) => response, async (error) => { if (error.response?.status === 401) { this.token = null; this.tokenExpiry = null; await this.ensureValidToken(); // Retry the original request const originalRequest = error.config; if (this.token) { originalRequest.headers.Authorization = `Bearer ${this.token.access_token}`; } return this.api.request(originalRequest); } throw error; }); } async ensureValidToken() { if (this.token && this.tokenExpiry && new Date() < this.tokenExpiry) { return; // Token is still valid } await this.getAccessToken(); } async getAccessToken() { try { const response = await axios.post(`${this.config.base_url}/oauth/access_token`, { grant_type: 'client_credentials', client_id: this.config.client_id, client_secret: this.config.client_secret, }); this.token = response.data; // Set expiry time (default to 1 hour if not provided) const expiresIn = this.token.expires_in || 3600; this.tokenExpiry = new Date(Date.now() + (expiresIn - 60) * 1000); // Refresh 1 minute early return this.token; } catch (error) { throw new Error(`Failed to get access token: ${error}`); } } // Customer Operations async getCustomers(input) { const params = new URLSearchParams({ page: input.page.toString(), 'per-page': '50', expand: 'contacts,custom_fields,locations', }); if (input.search) { params.append('search', input.search); } if (input.parent_customer) { params.append('filters[parent_customer]', input.parent_customer.toString()); } const response = await this.api.get(`/v1/customers?${params}`); return { items: response.data.items || [], _meta: response.data._meta, }; } async getCustomer(id) { const response = await this.api.get(`/v1/customers/${id}?expand=contacts,custom_fields,locations`); return response.data; } async createCustomer(input) { const validatedInput = CreateCustomerInputSchema.parse(input); const response = await this.api.post('/v1/customers', validatedInput); return response.data; } // Job Operations async getJobs(input) { const params = new URLSearchParams({ page: input.page.toString(), 'per-page': '50', expand: 'techs_assigned,tasks,notes,custom_fields', 'sort': '-updated_at', }); // Default to last 3 months if no date filter provided if (!input.updated_since) { const threeMonthsAgo = new Date(); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); const dateString = threeMonthsAgo.toISOString().split('T')[0]; params.append('filters[updated_date][gte]', `${dateString}T00:00:00-00:00`); } else { params.append('filters[updated_date][gte]', input.updated_since); } if (input.customer_id) { params.append('filters[customer_id]', input.customer_id.toString()); } if (input.customer_name) { params.append('filters[customer_name]', input.customer_name); } if (input.status) { params.append('filters[status]', input.status); } const response = await this.api.get(`/v1/jobs?${params}`); return { items: response.data.items || [], _meta: response.data._meta, }; } async getJob(id) { const response = await this.api.get(`/v1/jobs/${id}?expand=techs_assigned,tasks,notes,custom_fields`); return response.data; } async createJob(input) { const validatedInput = CreateJobInputSchema.parse(input); const response = await this.api.post('/v1/jobs', validatedInput); return response.data; } async updateJob(input) { const validatedInput = UpdateJobInputSchema.parse(input); const { job_id, ...updateData } = validatedInput; const response = await this.api.patch(`/v1/jobs/${job_id}`, updateData); return response.data; } async deleteJob(input) { const validatedInput = DeleteJobInputSchema.parse(input); await this.api.delete(`/v1/jobs/${validatedInput.job_id}`); return { success: true, message: `Job ${validatedInput.job_id} deleted successfully` }; } // Utility Methods async testConnection() { try { await this.ensureValidToken(); await this.api.get('/v1/customers?per-page=1'); return { success: true, message: 'ServiceFusion API connection successful' }; } catch (error) { return { success: false, message: `Connection failed: ${error}` }; } } async getApiStatus() { return { authenticated: !!this.token, token_expires: this.tokenExpiry?.toISOString() || null, base_url: this.config.base_url, }; } } //# sourceMappingURL=servicefusion-client.js.map