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
JavaScript
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