servicefusion-mcp
Version:
Model Context Protocol server for ServiceFusion API integration - enables AI agents to interact with ServiceFusion customers, jobs, and work orders
495 lines • 21.3 kB
JavaScript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { ServiceFusionClient } from './servicefusion-client.js';
import { ConfigSchema, } from './types.js';
class ServiceFusionMCPServer {
server;
client;
constructor() {
// Load configuration from environment variables
const config = ConfigSchema.parse({
client_id: process.env.SERVICEFUSION_CLIENT_ID,
client_secret: process.env.SERVICEFUSION_CLIENT_SECRET,
base_url: process.env.SERVICEFUSION_BASE_URL || 'https://api.servicefusion.com',
});
this.client = new ServiceFusionClient(config);
this.server = new Server({
name: 'servicefusion-mcp',
version: '1.0.4',
}, {
capabilities: {
tools: {
listChanged: true,
},
resources: {
subscribe: true,
listChanged: true,
},
},
});
this.setupHandlers();
}
setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'sf_test_connection',
description: 'Test connection to ServiceFusion API',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'sf_get_api_status',
description: 'Get current API authentication status',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'sf_create_customer',
description: 'Create a new customer in ServiceFusion',
inputSchema: {
type: 'object',
properties: {
customer_name: {
type: 'string',
description: 'Name of the customer',
},
parent_customer: {
type: 'number',
description: 'Parent customer ID for sub-customers (optional)',
},
contacts: {
type: 'array',
description: 'Customer contacts (optional)',
items: {
type: 'object',
properties: {
fname: { type: 'string' },
lname: { type: 'string' },
contact_type: { type: 'string' },
phone: { type: 'string' },
email: { type: 'string' },
},
},
},
locations: {
type: 'array',
description: 'Customer locations (optional)',
items: {
type: 'object',
properties: {
street_1: { type: 'string' },
street_2: { type: 'string' },
city: { type: 'string' },
state_prov: { type: 'string' },
postal_code: { type: 'string' },
},
},
},
},
required: ['customer_name'],
},
},
{
name: 'sf_create_job',
description: 'Create a new job/work order in ServiceFusion',
inputSchema: {
type: 'object',
properties: {
check_number: {
type: 'string',
description: 'Unique identifier for the job',
},
customer_id: {
type: 'number',
description: 'Customer ID for the job',
},
description: {
type: 'string',
description: 'Job description',
},
status: {
type: 'string',
description: 'Job status',
},
priority: {
type: 'string',
description: 'Job priority',
},
category: {
type: 'string',
description: 'Job category (optional)',
},
start_date: {
type: 'string',
description: 'Start date (YYYY-MM-DD) (optional)',
},
street_1: {
type: 'string',
description: 'Street address (optional)',
},
city: {
type: 'string',
description: 'City (optional)',
},
state_prov: {
type: 'string',
description: 'State/Province (optional)',
},
postal_code: {
type: 'string',
description: 'Postal code (optional)',
},
},
required: ['check_number', 'customer_id', 'description'],
},
},
{
name: 'sf_get_customers',
description: 'Get customers from ServiceFusion with optional filtering',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
search: {
type: 'string',
description: 'Search term for customer name (optional)',
},
parent_customer: {
type: 'number',
description: 'Filter by parent customer ID (optional)',
},
},
},
},
{
name: 'sf_get_jobs',
description: 'Get jobs/work orders from ServiceFusion with optional filtering',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
customer_id: {
type: 'number',
description: 'Filter by customer ID (optional)',
},
customer_name: {
type: 'string',
description: 'Filter by customer name (optional)',
},
status: {
type: 'string',
description: 'Filter by job status (optional)',
},
updated_since: {
type: 'string',
description: 'Filter by update date (ISO format) (optional)',
},
},
},
},
{
name: 'sf_update_job',
description: 'Update an existing job in ServiceFusion',
inputSchema: {
type: 'object',
properties: {
job_id: {
type: 'number',
description: 'Job ID to update',
},
status: {
type: 'string',
description: 'New job status (optional)',
},
completion_notes: {
type: 'string',
description: 'Completion notes (optional)',
},
tech_notes: {
type: 'string',
description: 'Tech notes (optional)',
},
},
required: ['job_id'],
},
},
{
name: 'sf_delete_job',
description: 'Delete/cancel a job in ServiceFusion',
inputSchema: {
type: 'object',
properties: {
job_id: {
type: 'number',
description: 'Job ID to delete',
},
},
required: ['job_id'],
},
},
],
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'sf_test_connection':
return {
content: [
{
type: 'text',
text: JSON.stringify(await this.client.testConnection(), null, 2),
},
],
};
case 'sf_get_api_status':
return {
content: [
{
type: 'text',
text: JSON.stringify(await this.client.getApiStatus(), null, 2),
},
],
};
case 'sf_create_customer':
const customerResult = await this.client.createCustomer(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(customerResult, null, 2),
},
],
};
case 'sf_create_job':
const jobResult = await this.client.createJob(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(jobResult, null, 2),
},
],
};
case 'sf_get_customers':
const customersResult = await this.client.getCustomers(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(customersResult, null, 2),
},
],
};
case 'sf_get_jobs':
const jobsResult = await this.client.getJobs(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(jobsResult, null, 2),
},
],
};
case 'sf_update_job':
const updateResult = await this.client.updateJob(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(updateResult, null, 2),
},
],
};
case 'sf_delete_job':
const deleteResult = await this.client.deleteJob(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(deleteResult, null, 2),
},
],
};
default:
throw new Error(`Unknown tool: ${name}`);
}
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
});
// List available resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: 'servicefusion://customers',
mimeType: 'application/json',
name: 'ServiceFusion Customers',
description: 'List of customers from ServiceFusion',
},
{
uri: 'servicefusion://jobs',
mimeType: 'application/json',
name: 'ServiceFusion Jobs',
description: 'List of jobs/work orders from ServiceFusion',
},
{
uri: 'servicefusion://customer/{id}',
mimeType: 'application/json',
name: 'ServiceFusion Customer Details',
description: 'Individual customer details by ID',
},
{
uri: 'servicefusion://job/{id}',
mimeType: 'application/json',
name: 'ServiceFusion Job Details',
description: 'Individual job details by ID',
},
{
uri: 'servicefusion://api-status',
mimeType: 'application/json',
name: 'ServiceFusion API Status',
description: 'Current API authentication and connection status',
},
],
}));
// Handle resource reads
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
if (uri === 'servicefusion://customers') {
const customers = await this.client.getCustomers({ page: 1 });
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(customers, null, 2),
},
],
};
}
if (uri === 'servicefusion://jobs') {
const jobs = await this.client.getJobs({ page: 1 });
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(jobs, null, 2),
},
],
};
}
if (uri === 'servicefusion://api-status') {
const status = await this.client.getApiStatus();
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(status, null, 2),
},
],
};
}
// Handle dynamic resources like customer/{id} and job/{id}
const customerMatch = uri.match(/^servicefusion:\/\/customer\/(\d+)$/);
if (customerMatch) {
const customerId = parseInt(customerMatch[1]);
const customer = await this.client.getCustomer(customerId);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(customer, null, 2),
},
],
};
}
const jobMatch = uri.match(/^servicefusion:\/\/job\/(\d+)$/);
if (jobMatch) {
const jobId = parseInt(jobMatch[1]);
const job = await this.client.getJob(jobId);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(job, null, 2),
},
],
};
}
throw new Error(`Unknown resource: ${uri}`);
}
catch (error) {
throw new Error(`Failed to read resource ${uri}: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('ServiceFusion MCP Server running on stdio');
}
}
// Error handling for missing environment variables
function validateEnvironment() {
const required = ['SERVICEFUSION_CLIENT_ID', 'SERVICEFUSION_CLIENT_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error('Missing required environment variables:');
missing.forEach(key => console.error(` - ${key}`));
console.error('\nPlease set these variables before starting the server.');
console.error('See .env.example for details.');
process.exit(1);
}
}
// Main execution
async function main() {
validateEnvironment();
const server = new ServiceFusionMCPServer();
await server.run();
}
// Handle graceful shutdown
process.on('SIGINT', () => {
console.error('\nShutting down ServiceFusion MCP Server...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.error('\nShutting down ServiceFusion MCP Server...');
process.exit(0);
});
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error('Failed to start server:', error);
process.exit(1);
});
}
//# sourceMappingURL=index.js.map