UNPKG

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
#!/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