diff --git a/servers/housecall-pro/README.md b/servers/housecall-pro/README.md new file mode 100644 index 0000000..ea19870 --- /dev/null +++ b/servers/housecall-pro/README.md @@ -0,0 +1,381 @@ +# Housecall Pro MCP Server + +Complete Model Context Protocol server for Housecall Pro field service management platform. + +## Features + +### 🛠️ **47 Tools Across 10 Categories** + +#### Job Management (10 tools) +- `list_jobs` - List jobs with filters (status, customer, dates, tags) +- `get_job` - Get detailed job information +- `create_job` - Create new jobs +- `update_job` - Update job details +- `complete_job` - Mark jobs as completed +- `cancel_job` - Cancel jobs with reason +- `list_job_line_items` - List line items for a job +- `add_job_line_item` - Add line items to jobs +- `schedule_job` - Schedule jobs +- `reschedule_job` - Reschedule existing jobs + +#### Customer Management (7 tools) +- `list_customers` - List customers with filters +- `get_customer` - Get customer details +- `create_customer` - Create new customers +- `update_customer` - Update customer information +- `delete_customer` - Delete customers +- `search_customers` - Search by name, email, phone +- `list_customer_addresses` - List customer addresses + +#### Estimate Management (8 tools) +- `list_estimates` - List estimates with filters +- `get_estimate` - Get estimate details +- `create_estimate` - Create new estimates +- `update_estimate` - Update estimate details +- `send_estimate` - Send estimates to customers +- `approve_estimate` - Mark estimates as approved +- `decline_estimate` - Decline estimates +- `convert_estimate_to_job` - Convert approved estimates to jobs + +#### Invoice Management (6 tools) +- `list_invoices` - List invoices with filters +- `get_invoice` - Get invoice details +- `create_invoice` - Create new invoices +- `send_invoice` - Send invoices to customers +- `mark_invoice_paid` - Record invoice payments +- `list_invoice_payments` - List all payments for an invoice + +#### Employee Management (6 tools) +- `list_employees` - List employees with filters +- `get_employee` - Get employee details +- `create_employee` - Create new employees +- `update_employee` - Update employee information +- `get_employee_schedule` - Get employee schedules +- `list_employee_time_entries` - List time entries + +#### Dispatch & Scheduling (3 tools) +- `get_dispatch_board` - Get dispatch board view +- `assign_employee_to_job` - Assign employees to jobs +- `get_employee_availability` - Check employee availability + +#### Tag Management (5 tools) +- `list_tags` - List all tags +- `create_tag` - Create new tags +- `delete_tag` - Delete tags +- `add_tag_to_job` - Tag jobs +- `add_tag_to_customer` - Tag customers + +#### Notification Management (3 tools) +- `list_notifications` - List sent notifications +- `send_notification` - Send SMS/email notifications +- `mark_notification_read` - Mark notifications as read + +#### Review Management (3 tools) +- `list_reviews` - List customer reviews +- `get_review` - Get review details +- `request_review` - Request reviews from customers + +#### Reporting & Analytics (3 tools) +- `get_revenue_report` - Revenue analytics +- `get_job_completion_report` - Job completion metrics +- `get_employee_performance_report` - Employee performance stats + +## Installation + +```bash +npm install @mcpengine/housecall-pro +``` + +## Configuration + +Set the required environment variable: + +```bash +export HOUSECALL_PRO_API_KEY="your-api-key-here" +``` + +Optional configuration: + +```bash +export HOUSECALL_PRO_BASE_URL="https://api.housecallpro.com" # default +``` + +## Usage + +### As MCP Server + +Add to your MCP settings file: + +```json +{ + "mcpServers": { + "housecall-pro": { + "command": "npx", + "args": ["-y", "@mcpengine/housecall-pro"], + "env": { + "HOUSECALL_PRO_API_KEY": "your-api-key-here" + } + } + } +} +``` + +### Programmatic Usage + +```typescript +import { HousecallProClient } from '@mcpengine/housecall-pro'; + +const client = new HousecallProClient({ + apiKey: process.env.HOUSECALL_PRO_API_KEY!, +}); + +// List jobs +const jobs = await client.listJobs({ + work_status: 'scheduled', + start_date: '2024-01-01', + end_date: '2024-01-31', +}); + +// Create a customer +const customer = await client.createCustomer({ + first_name: 'John', + last_name: 'Doe', + email: 'john@example.com', + mobile_number: '+1234567890', +}); + +// Create an estimate +const estimate = await client.createEstimate({ + customer_id: customer.id, + line_items: [ + { + name: 'HVAC Repair', + description: 'Replace thermostat', + quantity: 1, + unit_price: 250.00, + }, + ], +}); + +// Send the estimate +await client.sendEstimate(estimate.id); +``` + +## API Reference + +### Client Initialization + +```typescript +const client = new HousecallProClient({ + apiKey: string; // Required: Your Housecall Pro API key + baseUrl?: string; // Optional: API base URL (default: https://api.housecallpro.com) +}); +``` + +### Error Handling + +The client throws `APIError` objects with the following structure: + +```typescript +{ + error: 'APIError' | 'NetworkError' | 'ExecutionError'; + message: string; + status: number; + details?: any; +} +``` + +Example error handling: + +```typescript +try { + const job = await client.getJob('job-123'); +} catch (error: any) { + if (error.error === 'APIError') { + console.error(`API Error ${error.status}: ${error.message}`); + } else { + console.error(`Error: ${error.message}`); + } +} +``` + +### Pagination + +List methods support pagination: + +```typescript +// Get single page +const page = await client.getPage('/jobs', { + page: 1, + page_size: 50, +}); + +// Get all pages (auto-pagination) +const allJobs = await client.listJobs({ + page_size: 100, // Items per page +}); +``` + +## Tool Examples + +### Job Management + +```typescript +// Create and schedule a job +const job = await client.createJob({ + customer_id: 'cust-123', + description: 'Annual HVAC maintenance', + schedule_start: '2024-02-15T09:00:00Z', + schedule_end: '2024-02-15T11:00:00Z', + assigned_employees: ['emp-456'], +}); + +// Add line items +await client.addJobLineItem(job.id, { + name: 'Filter Replacement', + quantity: 2, + unit_price: 45.00, +}); + +// Complete the job +await client.completeJob(job.id); +``` + +### Customer & Address Management + +```typescript +// Create customer with tags +const customer = await client.createCustomer({ + first_name: 'Jane', + last_name: 'Smith', + email: 'jane@example.com', + tags: ['vip', 'commercial'], +}); + +// Get customer addresses +const addresses = await client.listCustomerAddresses(customer.id); +``` + +### Estimates & Invoices + +```typescript +// Create estimate +const estimate = await client.createEstimate({ + customer_id: 'cust-123', + line_items: [ + { name: 'Labor', quantity: 2, unit_price: 85.00 }, + { name: 'Parts', quantity: 1, unit_price: 150.00 }, + ], + valid_until: '2024-03-01', +}); + +// Send and track +await client.sendEstimate(estimate.id); +await client.approveEstimate(estimate.id); +const job = await client.convertEstimateToJob(estimate.id); + +// Create invoice from job +const invoice = await client.createInvoice({ + job_id: job.id, + due_date: '2024-02-28', +}); + +// Record payment +await client.markInvoicePaid(invoice.id, { + amount: 320.00, + method: 'card', + reference: 'ch_1234567890', +}); +``` + +### Dispatch & Scheduling + +```typescript +// Check employee availability +const availability = await client.getEmployeeAvailability('emp-123', { + start_date: '2024-02-15', + end_date: '2024-02-21', +}); + +// View dispatch board +const board = await client.getDispatchBoard({ + date: '2024-02-15', + employee_ids: ['emp-123', 'emp-456'], +}); + +// Assign employee +await client.assignEmployee('job-789', 'emp-123'); +``` + +### Reports & Analytics + +```typescript +// Revenue report +const revenue = await client.getRevenueReport({ + start_date: '2024-01-01', + end_date: '2024-01-31', + group_by: 'day', +}); + +// Job completion metrics +const completion = await client.getJobCompletionReport({ + start_date: '2024-01-01', + end_date: '2024-01-31', +}); + +// Employee performance +const performance = await client.getEmployeePerformanceReport({ + employee_id: 'emp-123', + start_date: '2024-01-01', + end_date: '2024-01-31', +}); +``` + +## MCP Apps (UI Components) + +This server includes 16 React-based MCP apps for rich UI interactions: + +- **job-dashboard** - Overview of all jobs +- **job-detail** - Detailed job view +- **job-grid** - Grid view of jobs +- **customer-detail** - Customer profile view +- **customer-grid** - Customer list/grid +- **estimate-builder** - Interactive estimate creation +- **estimate-grid** - Estimate list/grid +- **invoice-dashboard** - Invoice overview +- **invoice-detail** - Detailed invoice view +- **dispatch-board** - Visual dispatch/scheduling board +- **employee-schedule** - Employee schedule calendar +- **employee-performance** - Performance metrics dashboard +- **review-dashboard** - Review management +- **revenue-dashboard** - Revenue analytics +- **tag-manager** - Tag organization +- **notification-center** - Notification management + +## Development + +```bash +# Install dependencies +npm install + +# Build +npm run build + +# Development (watch mode) +npm run dev + +# Run server +npm start +``` + +## API Documentation + +For complete API documentation, visit: https://docs.housecallpro.com/ + +## License + +MIT + +## Support + +For issues and feature requests, please visit: https://github.com/mcpengine/housecall-pro-mcp diff --git a/servers/housecall-pro/package.json b/servers/housecall-pro/package.json new file mode 100644 index 0000000..c575da0 --- /dev/null +++ b/servers/housecall-pro/package.json @@ -0,0 +1,38 @@ +{ + "name": "@mcpengine/housecall-pro", + "version": "1.0.0", + "description": "Housecall Pro MCP Server - Complete field service management integration", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "housecall-pro-mcp": "dist/main.js" + }, + "scripts": { + "build": "tsc && chmod +x dist/main.js", + "dev": "tsc --watch", + "start": "node dist/main.js", + "clean": "rm -rf dist" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "housecall-pro", + "field-service", + "scheduling", + "dispatch", + "crm" + ], + "author": "MCPEngine", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "typescript": "^5.7.3" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/servers/housecall-pro/src/clients/housecall-pro.ts b/servers/housecall-pro/src/clients/housecall-pro.ts new file mode 100644 index 0000000..50e9d00 --- /dev/null +++ b/servers/housecall-pro/src/clients/housecall-pro.ts @@ -0,0 +1,480 @@ +/** + * Housecall Pro API Client + * https://api.housecallpro.com/ + */ + +import { + HousecallProConfig, + PaginationParams, + PaginatedResponse, + APIError, +} from '../types/index.js'; + +export class HousecallProClient { + private apiKey: string; + private baseUrl: string; + + constructor(config: HousecallProConfig) { + this.apiKey = config.apiKey; + this.baseUrl = config.baseUrl || 'https://api.housecallpro.com'; + } + + /** + * Make authenticated request to Housecall Pro API + */ + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}`; + + const headers: Record = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...options.headers as Record, + }; + + try { + const response = await fetch(url, { + ...options, + headers, + }); + + if (!response.ok) { + const errorBody = await response.text(); + let errorMessage = `HTTP ${response.status}: ${response.statusText}`; + + try { + const errorJson = JSON.parse(errorBody); + errorMessage = errorJson.message || errorJson.error || errorMessage; + } catch { + // Use default error message + } + + const error: APIError = { + error: 'APIError', + message: errorMessage, + status: response.status, + details: errorBody, + }; + throw error; + } + + const data = await response.json(); + return data as T; + } catch (error: any) { + if (error.error === 'APIError') { + throw error; + } + + const apiError: APIError = { + error: 'NetworkError', + message: error.message || 'Network request failed', + status: 0, + details: error, + }; + throw apiError; + } + } + + /** + * GET request + */ + async get(endpoint: string, params?: Record): Promise { + let url = endpoint; + + if (params) { + const queryParams = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + queryParams.append(key, String(value)); + } + }); + const queryString = queryParams.toString(); + if (queryString) { + url += `?${queryString}`; + } + } + + return this.request(url, { method: 'GET' }); + } + + /** + * POST request + */ + async post(endpoint: string, body?: any): Promise { + return this.request(endpoint, { + method: 'POST', + body: body ? JSON.stringify(body) : undefined, + }); + } + + /** + * PUT request + */ + async put(endpoint: string, body?: any): Promise { + return this.request(endpoint, { + method: 'PUT', + body: body ? JSON.stringify(body) : undefined, + }); + } + + /** + * PATCH request + */ + async patch(endpoint: string, body?: any): Promise { + return this.request(endpoint, { + method: 'PATCH', + body: body ? JSON.stringify(body) : undefined, + }); + } + + /** + * DELETE request + */ + async delete(endpoint: string): Promise { + return this.request(endpoint, { method: 'DELETE' }); + } + + /** + * Paginated GET request - fetches all pages + */ + async getPaginated( + endpoint: string, + params?: PaginationParams & Record + ): Promise { + const allItems: T[] = []; + let currentPage = params?.page || 1; + const pageSize = params?.page_size || 50; + + // Create params without page/page_size for first request + const { page, page_size, ...otherParams } = params || {}; + + while (true) { + const response = await this.get>(endpoint, { + ...otherParams, + page: currentPage, + page_size: pageSize, + }); + + allItems.push(...response.data); + + // Check if we've reached the last page + if (currentPage >= response.total_pages) { + break; + } + + currentPage++; + } + + return allItems; + } + + /** + * Paginated GET request - single page + */ + async getPage( + endpoint: string, + params?: PaginationParams & Record + ): Promise> { + return this.get>(endpoint, params); + } + + // ===== Customer Endpoints ===== + + async listCustomers(params?: PaginationParams & { + search?: string; + tags?: string[]; + lead_source?: string; + }) { + return this.getPaginated('/customers', params); + } + + async getCustomer(customerId: string) { + return this.get(`/customers/${customerId}`); + } + + async createCustomer(data: any) { + return this.post('/customers', data); + } + + async updateCustomer(customerId: string, data: any) { + return this.patch(`/customers/${customerId}`, data); + } + + async deleteCustomer(customerId: string) { + return this.delete(`/customers/${customerId}`); + } + + async searchCustomers(query: string, params?: PaginationParams) { + return this.getPaginated('/customers', { ...params, search: query }); + } + + async listCustomerAddresses(customerId: string) { + return this.get(`/customers/${customerId}/addresses`); + } + + // ===== Job Endpoints ===== + + async listJobs(params?: PaginationParams & { + customer_id?: string; + work_status?: string; + invoice_status?: string; + start_date?: string; + end_date?: string; + tags?: string[]; + }) { + return this.getPaginated('/jobs', params); + } + + async getJob(jobId: string) { + return this.get(`/jobs/${jobId}`); + } + + async createJob(data: any) { + return this.post('/jobs', data); + } + + async updateJob(jobId: string, data: any) { + return this.patch(`/jobs/${jobId}`, data); + } + + async completeJob(jobId: string) { + return this.post(`/jobs/${jobId}/complete`, {}); + } + + async cancelJob(jobId: string, reason?: string) { + return this.post(`/jobs/${jobId}/cancel`, { reason }); + } + + async listJobLineItems(jobId: string) { + return this.get(`/jobs/${jobId}/line_items`); + } + + async addJobLineItem(jobId: string, data: any) { + return this.post(`/jobs/${jobId}/line_items`, data); + } + + async scheduleJob(jobId: string, schedule: any) { + return this.post(`/jobs/${jobId}/schedule`, schedule); + } + + async rescheduleJob(jobId: string, schedule: any) { + return this.patch(`/jobs/${jobId}/schedule`, schedule); + } + + // ===== Estimate Endpoints ===== + + async listEstimates(params?: PaginationParams & { + customer_id?: string; + status?: string; + }) { + return this.getPaginated('/estimates', params); + } + + async getEstimate(estimateId: string) { + return this.get(`/estimates/${estimateId}`); + } + + async createEstimate(data: any) { + return this.post('/estimates', data); + } + + async updateEstimate(estimateId: string, data: any) { + return this.patch(`/estimates/${estimateId}`, data); + } + + async sendEstimate(estimateId: string, options?: { email?: string; message?: string }) { + return this.post(`/estimates/${estimateId}/send`, options); + } + + async approveEstimate(estimateId: string) { + return this.post(`/estimates/${estimateId}/approve`, {}); + } + + async declineEstimate(estimateId: string, reason?: string) { + return this.post(`/estimates/${estimateId}/decline`, { reason }); + } + + async convertEstimateToJob(estimateId: string) { + return this.post(`/estimates/${estimateId}/convert`, {}); + } + + // ===== Invoice Endpoints ===== + + async listInvoices(params?: PaginationParams & { + customer_id?: string; + job_id?: string; + status?: string; + start_date?: string; + end_date?: string; + }) { + return this.getPaginated('/invoices', params); + } + + async getInvoice(invoiceId: string) { + return this.get(`/invoices/${invoiceId}`); + } + + async createInvoice(data: any) { + return this.post('/invoices', data); + } + + async sendInvoice(invoiceId: string, options?: { email?: string; message?: string }) { + return this.post(`/invoices/${invoiceId}/send`, options); + } + + async markInvoicePaid(invoiceId: string, payment: any) { + return this.post(`/invoices/${invoiceId}/payments`, payment); + } + + async listInvoicePayments(invoiceId: string) { + return this.get(`/invoices/${invoiceId}/payments`); + } + + // ===== Employee Endpoints ===== + + async listEmployees(params?: PaginationParams & { + is_active?: boolean; + role?: string; + }) { + return this.getPaginated('/employees', params); + } + + async getEmployee(employeeId: string) { + return this.get(`/employees/${employeeId}`); + } + + async createEmployee(data: any) { + return this.post('/employees', data); + } + + async updateEmployee(employeeId: string, data: any) { + return this.patch(`/employees/${employeeId}`, data); + } + + async getEmployeeSchedule(employeeId: string, params?: { + start_date?: string; + end_date?: string; + }) { + return this.get(`/employees/${employeeId}/schedule`, params); + } + + async listEmployeeTimeEntries(employeeId: string, params?: PaginationParams & { + start_date?: string; + end_date?: string; + }) { + return this.getPaginated(`/employees/${employeeId}/time_entries`, params); + } + + // ===== Dispatch Endpoints ===== + + async getDispatchBoard(params?: { + date?: string; + employee_ids?: string[]; + }) { + return this.get('/dispatch/board', params); + } + + async assignEmployee(jobId: string, employeeId: string) { + return this.post(`/jobs/${jobId}/assign`, { employee_id: employeeId }); + } + + async getEmployeeAvailability(employeeId: string, params?: { + start_date?: string; + end_date?: string; + }) { + return this.get(`/employees/${employeeId}/availability`, params); + } + + // ===== Tag Endpoints ===== + + async listTags(params?: PaginationParams) { + return this.getPaginated('/tags', params); + } + + async createTag(data: { name: string; color?: string }) { + return this.post('/tags', data); + } + + async deleteTag(tagId: string) { + return this.delete(`/tags/${tagId}`); + } + + async addTagToJob(jobId: string, tagId: string) { + return this.post(`/jobs/${jobId}/tags`, { tag_id: tagId }); + } + + async addTagToCustomer(customerId: string, tagId: string) { + return this.post(`/customers/${customerId}/tags`, { tag_id: tagId }); + } + + // ===== Notification Endpoints ===== + + async listNotifications(params?: PaginationParams & { + type?: string; + status?: string; + }) { + return this.getPaginated('/notifications', params); + } + + async sendNotification(data: { + type: 'sms' | 'email'; + recipient: string; + subject?: string; + message: string; + }) { + return this.post('/notifications', data); + } + + async markNotificationRead(notificationId: string) { + return this.patch(`/notifications/${notificationId}`, { read: true }); + } + + // ===== Review Endpoints ===== + + async listReviews(params?: PaginationParams & { + customer_id?: string; + job_id?: string; + rating?: number; + }) { + return this.getPaginated('/reviews', params); + } + + async getReview(reviewId: string) { + return this.get(`/reviews/${reviewId}`); + } + + async requestReview(jobId: string, options?: { + method?: 'sms' | 'email'; + message?: string; + }) { + return this.post(`/jobs/${jobId}/request_review`, options); + } + + // ===== Reporting Endpoints ===== + + async getRevenueReport(params: { + start_date: string; + end_date: string; + group_by?: 'day' | 'week' | 'month'; + }) { + return this.get('/reports/revenue', params); + } + + async getJobCompletionReport(params: { + start_date: string; + end_date: string; + }) { + return this.get('/reports/job_completion', params); + } + + async getEmployeePerformanceReport(params: { + employee_id?: string; + start_date: string; + end_date: string; + }) { + return this.get('/reports/employee_performance', params); + } +} diff --git a/servers/housecall-pro/src/index.ts b/servers/housecall-pro/src/index.ts new file mode 100644 index 0000000..22a3884 --- /dev/null +++ b/servers/housecall-pro/src/index.ts @@ -0,0 +1,7 @@ +/** + * Housecall Pro MCP Server - Main Exports + */ + +export { HousecallProServer } from './server.js'; +export { HousecallProClient } from './clients/housecall-pro.js'; +export * from './types/index.js'; diff --git a/servers/housecall-pro/src/main.ts b/servers/housecall-pro/src/main.ts new file mode 100644 index 0000000..fafe257 --- /dev/null +++ b/servers/housecall-pro/src/main.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +/** + * Housecall Pro MCP Server Entry Point + */ + +import { HousecallProServer } from './server.js'; + +async function main() { + try { + const server = new HousecallProServer(); + await server.run(); + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +} + +main(); diff --git a/servers/housecall-pro/src/server.ts b/servers/housecall-pro/src/server.ts new file mode 100644 index 0000000..9a94d33 --- /dev/null +++ b/servers/housecall-pro/src/server.ts @@ -0,0 +1,136 @@ +/** + * Housecall Pro MCP Server + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { HousecallProClient } from './clients/housecall-pro.js'; +import { registerAllTools } from './tools/index.js'; + +export class HousecallProServer { + private server: Server; + private client: HousecallProClient; + private tools: Record; + + constructor() { + this.server = new Server( + { + name: 'housecall-pro', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + resources: {}, + }, + } + ); + + // Initialize client (API key will be set from env var) + const apiKey = process.env.HOUSECALL_PRO_API_KEY; + if (!apiKey) { + throw new Error('HOUSECALL_PRO_API_KEY environment variable is required'); + } + + this.client = new HousecallProClient({ + apiKey, + baseUrl: process.env.HOUSECALL_PRO_BASE_URL, + }); + + // Register all tools + this.tools = registerAllTools(this.client); + + this.setupHandlers(); + } + + private setupHandlers() { + // List available tools + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: Object.entries(this.tools).map(([name, tool]) => ({ + name, + description: tool.description, + inputSchema: tool.parameters, + })), + }; + }); + + // Handle tool execution + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + const tool = this.tools[name]; + if (!tool) { + throw new Error(`Unknown tool: ${name}`); + } + + try { + const result = await tool.handler(args || {}); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error: any) { + // Handle API errors + if (error.error === 'APIError') { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + error: error.error, + message: error.message, + status: error.status, + }, null, 2), + }, + ], + isError: true, + }; + } + + // Handle other errors + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + error: 'ExecutionError', + message: error.message || 'Tool execution failed', + details: error, + }, null, 2), + }, + ], + isError: true, + }; + } + }); + + // List resources (empty for now, but could include templates, reports, etc.) + this.server.setRequestHandler(ListResourcesRequestSchema, async () => { + return { + resources: [], + }; + }); + + // Read resource (empty for now) + this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + throw new Error(`Resource not found: ${request.params.uri}`); + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('Housecall Pro MCP server running on stdio'); + } +} diff --git a/servers/housecall-pro/src/tools/customers-tools.ts b/servers/housecall-pro/src/tools/customers-tools.ts new file mode 100644 index 0000000..8d9f9a3 --- /dev/null +++ b/servers/housecall-pro/src/tools/customers-tools.ts @@ -0,0 +1,242 @@ +/** + * Customer Management Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { Customer, CreateCustomerRequest } from '../types/index.js'; + +export function registerCustomersTools(client: HousecallProClient) { + return { + list_customers: { + description: 'List customers with optional filters', + parameters: { + type: 'object', + properties: { + search: { + type: 'string', + description: 'Search query (name, email, phone)', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Filter by tags', + }, + lead_source: { + type: 'string', + description: 'Filter by lead source', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + }, + handler: async (params: any) => { + const customers = await client.listCustomers(params); + return { customers, count: customers.length }; + }, + }, + + get_customer: { + description: 'Get detailed information about a specific customer', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Customer ID', + }, + }, + required: ['customer_id'], + }, + handler: async (params: { customer_id: string }) => { + const customer = await client.getCustomer(params.customer_id); + return customer; + }, + }, + + create_customer: { + description: 'Create a new customer', + parameters: { + type: 'object', + properties: { + first_name: { + type: 'string', + description: 'First name', + }, + last_name: { + type: 'string', + description: 'Last name', + }, + email: { + type: 'string', + description: 'Email address', + }, + mobile_number: { + type: 'string', + description: 'Mobile phone number', + }, + home_number: { + type: 'string', + description: 'Home phone number', + }, + work_number: { + type: 'string', + description: 'Work phone number', + }, + company: { + type: 'string', + description: 'Company name', + }, + notifications_enabled: { + type: 'boolean', + description: 'Enable notifications for this customer', + }, + lead_source: { + type: 'string', + description: 'Lead source', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Array of tag IDs', + }, + }, + required: ['first_name', 'last_name'], + }, + handler: async (params: CreateCustomerRequest) => { + const customer = await client.createCustomer(params); + return customer; + }, + }, + + update_customer: { + description: 'Update an existing customer', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Customer ID', + }, + first_name: { + type: 'string', + description: 'First name', + }, + last_name: { + type: 'string', + description: 'Last name', + }, + email: { + type: 'string', + description: 'Email address', + }, + mobile_number: { + type: 'string', + description: 'Mobile phone number', + }, + home_number: { + type: 'string', + description: 'Home phone number', + }, + work_number: { + type: 'string', + description: 'Work phone number', + }, + company: { + type: 'string', + description: 'Company name', + }, + notifications_enabled: { + type: 'boolean', + description: 'Enable notifications for this customer', + }, + lead_source: { + type: 'string', + description: 'Lead source', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Array of tag IDs', + }, + }, + required: ['customer_id'], + }, + handler: async (params: any) => { + const { customer_id, ...updateData } = params; + const customer = await client.updateCustomer(customer_id, updateData); + return customer; + }, + }, + + delete_customer: { + description: 'Delete a customer', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Customer ID', + }, + }, + required: ['customer_id'], + }, + handler: async (params: { customer_id: string }) => { + const result = await client.deleteCustomer(params.customer_id); + return result; + }, + }, + + search_customers: { + description: 'Search customers by name, email, or phone', + parameters: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + required: ['query'], + }, + handler: async (params: any) => { + const customers = await client.searchCustomers(params.query, { + page: params.page, + page_size: params.page_size, + }); + return { customers, count: customers.length }; + }, + }, + + list_customer_addresses: { + description: 'List all addresses for a customer', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Customer ID', + }, + }, + required: ['customer_id'], + }, + handler: async (params: { customer_id: string }) => { + const addresses = await client.listCustomerAddresses(params.customer_id); + return addresses; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/tools/dispatch-tools.ts b/servers/housecall-pro/src/tools/dispatch-tools.ts new file mode 100644 index 0000000..24b34c2 --- /dev/null +++ b/servers/housecall-pro/src/tools/dispatch-tools.ts @@ -0,0 +1,80 @@ +/** + * Dispatch and Scheduling Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; + +export function registerDispatchTools(client: HousecallProClient) { + return { + get_dispatch_board: { + description: 'Get the dispatch board showing scheduled jobs and employee assignments', + parameters: { + type: 'object', + properties: { + date: { + type: 'string', + description: 'Date for dispatch board (ISO 8601 date, e.g. "2024-01-15")', + }, + employee_ids: { + type: 'array', + items: { type: 'string' }, + description: 'Filter by specific employee IDs', + }, + }, + }, + handler: async (params: any) => { + const board = await client.getDispatchBoard(params); + return board; + }, + }, + + assign_employee_to_job: { + description: 'Assign an employee to a job', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + employee_id: { + type: 'string', + description: 'Employee ID to assign', + }, + }, + required: ['job_id', 'employee_id'], + }, + handler: async (params: { job_id: string; employee_id: string }) => { + const result = await client.assignEmployee(params.job_id, params.employee_id); + return result; + }, + }, + + get_employee_availability: { + description: 'Get availability slots for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + start_date: { + type: 'string', + description: 'Start date for availability (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date for availability (ISO 8601)', + }, + }, + required: ['employee_id'], + }, + handler: async (params: any) => { + const { employee_id, ...dateParams } = params; + const availability = await client.getEmployeeAvailability(employee_id, dateParams); + return availability; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/tools/employees-tools.ts b/servers/housecall-pro/src/tools/employees-tools.ts new file mode 100644 index 0000000..885a566 --- /dev/null +++ b/servers/housecall-pro/src/tools/employees-tools.ts @@ -0,0 +1,207 @@ +/** + * Employee Management Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { Employee, CreateEmployeeRequest, TimeEntry } from '../types/index.js'; + +export function registerEmployeesTools(client: HousecallProClient) { + return { + list_employees: { + description: 'List employees with optional filters', + parameters: { + type: 'object', + properties: { + is_active: { + type: 'boolean', + description: 'Filter by active status', + }, + role: { + type: 'string', + enum: ['admin', 'dispatcher', 'technician', 'sales'], + description: 'Filter by role', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + }, + handler: async (params: any) => { + const employees = await client.listEmployees(params); + return { employees, count: employees.length }; + }, + }, + + get_employee: { + description: 'Get detailed information about a specific employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + }, + required: ['employee_id'], + }, + handler: async (params: { employee_id: string }) => { + const employee = await client.getEmployee(params.employee_id); + return employee; + }, + }, + + create_employee: { + description: 'Create a new employee', + parameters: { + type: 'object', + properties: { + first_name: { + type: 'string', + description: 'First name', + }, + last_name: { + type: 'string', + description: 'Last name', + }, + email: { + type: 'string', + description: 'Email address', + }, + mobile_number: { + type: 'string', + description: 'Mobile phone number', + }, + role: { + type: 'string', + enum: ['admin', 'dispatcher', 'technician', 'sales'], + description: 'Employee role', + }, + color: { + type: 'string', + description: 'Color for calendar display (hex code)', + }, + }, + required: ['first_name', 'last_name', 'email', 'role'], + }, + handler: async (params: CreateEmployeeRequest) => { + const employee = await client.createEmployee(params); + return employee; + }, + }, + + update_employee: { + description: 'Update an existing employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + first_name: { + type: 'string', + description: 'First name', + }, + last_name: { + type: 'string', + description: 'Last name', + }, + email: { + type: 'string', + description: 'Email address', + }, + mobile_number: { + type: 'string', + description: 'Mobile phone number', + }, + role: { + type: 'string', + enum: ['admin', 'dispatcher', 'technician', 'sales'], + description: 'Employee role', + }, + is_active: { + type: 'boolean', + description: 'Active status', + }, + color: { + type: 'string', + description: 'Color for calendar display (hex code)', + }, + }, + required: ['employee_id'], + }, + handler: async (params: any) => { + const { employee_id, ...updateData } = params; + const employee = await client.updateEmployee(employee_id, updateData); + return employee; + }, + }, + + get_employee_schedule: { + description: 'Get schedule for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + start_date: { + type: 'string', + description: 'Start date for schedule (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date for schedule (ISO 8601)', + }, + }, + required: ['employee_id'], + }, + handler: async (params: any) => { + const { employee_id, ...dateParams } = params; + const schedule = await client.getEmployeeSchedule(employee_id, dateParams); + return schedule; + }, + }, + + list_employee_time_entries: { + description: 'List time entries for an employee', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID', + }, + start_date: { + type: 'string', + description: 'Start date for time entries (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date for time entries (ISO 8601)', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + required: ['employee_id'], + }, + handler: async (params: any) => { + const { employee_id, ...queryParams } = params; + const timeEntries = await client.listEmployeeTimeEntries(employee_id, queryParams); + return { time_entries: timeEntries, count: timeEntries.length }; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/tools/estimates-tools.ts b/servers/housecall-pro/src/tools/estimates-tools.ts new file mode 100644 index 0000000..fa961c8 --- /dev/null +++ b/servers/housecall-pro/src/tools/estimates-tools.ts @@ -0,0 +1,227 @@ +/** + * Estimate Management Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { Estimate, CreateEstimateRequest } from '../types/index.js'; + +export function registerEstimatesTools(client: HousecallProClient) { + return { + list_estimates: { + description: 'List estimates with optional filters', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Filter by customer ID', + }, + status: { + type: 'string', + enum: ['draft', 'sent', 'approved', 'declined', 'expired'], + description: 'Filter by status', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + }, + handler: async (params: any) => { + const estimates = await client.listEstimates(params); + return { estimates, count: estimates.length }; + }, + }, + + get_estimate: { + description: 'Get detailed information about a specific estimate', + parameters: { + type: 'object', + properties: { + estimate_id: { + type: 'string', + description: 'Estimate ID', + }, + }, + required: ['estimate_id'], + }, + handler: async (params: { estimate_id: string }) => { + const estimate = await client.getEstimate(params.estimate_id); + return estimate; + }, + }, + + create_estimate: { + description: 'Create a new estimate', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Customer ID', + }, + address_id: { + type: 'string', + description: 'Service address ID', + }, + line_items: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + quantity: { type: 'number' }, + unit_price: { type: 'number' }, + }, + required: ['name', 'quantity', 'unit_price'], + }, + description: 'Array of line items', + }, + notes: { + type: 'string', + description: 'Estimate notes', + }, + valid_until: { + type: 'string', + description: 'Estimate expiration date (ISO 8601)', + }, + }, + required: ['customer_id'], + }, + handler: async (params: CreateEstimateRequest) => { + const estimate = await client.createEstimate(params); + return estimate; + }, + }, + + update_estimate: { + description: 'Update an existing estimate', + parameters: { + type: 'object', + properties: { + estimate_id: { + type: 'string', + description: 'Estimate ID', + }, + line_items: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + quantity: { type: 'number' }, + unit_price: { type: 'number' }, + }, + }, + description: 'Array of line items', + }, + notes: { + type: 'string', + description: 'Estimate notes', + }, + valid_until: { + type: 'string', + description: 'Estimate expiration date (ISO 8601)', + }, + }, + required: ['estimate_id'], + }, + handler: async (params: any) => { + const { estimate_id, ...updateData } = params; + const estimate = await client.updateEstimate(estimate_id, updateData); + return estimate; + }, + }, + + send_estimate: { + description: 'Send an estimate to the customer', + parameters: { + type: 'object', + properties: { + estimate_id: { + type: 'string', + description: 'Estimate ID', + }, + email: { + type: 'string', + description: 'Override customer email', + }, + message: { + type: 'string', + description: 'Custom message to include', + }, + }, + required: ['estimate_id'], + }, + handler: async (params: any) => { + const { estimate_id, ...options } = params; + const result = await client.sendEstimate(estimate_id, options); + return result; + }, + }, + + approve_estimate: { + description: 'Mark an estimate as approved', + parameters: { + type: 'object', + properties: { + estimate_id: { + type: 'string', + description: 'Estimate ID', + }, + }, + required: ['estimate_id'], + }, + handler: async (params: { estimate_id: string }) => { + const result = await client.approveEstimate(params.estimate_id); + return result; + }, + }, + + decline_estimate: { + description: 'Mark an estimate as declined', + parameters: { + type: 'object', + properties: { + estimate_id: { + type: 'string', + description: 'Estimate ID', + }, + reason: { + type: 'string', + description: 'Reason for declining', + }, + }, + required: ['estimate_id'], + }, + handler: async (params: { estimate_id: string; reason?: string }) => { + const result = await client.declineEstimate(params.estimate_id, params.reason); + return result; + }, + }, + + convert_estimate_to_job: { + description: 'Convert an approved estimate into a job', + parameters: { + type: 'object', + properties: { + estimate_id: { + type: 'string', + description: 'Estimate ID', + }, + }, + required: ['estimate_id'], + }, + handler: async (params: { estimate_id: string }) => { + const job = await client.convertEstimateToJob(params.estimate_id); + return job; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/tools/index.ts b/servers/housecall-pro/src/tools/index.ts new file mode 100644 index 0000000..67f186f --- /dev/null +++ b/servers/housecall-pro/src/tools/index.ts @@ -0,0 +1,30 @@ +/** + * Tools Registry + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { registerJobsTools } from './jobs-tools.js'; +import { registerCustomersTools } from './customers-tools.js'; +import { registerEstimatesTools } from './estimates-tools.js'; +import { registerInvoicesTools } from './invoices-tools.js'; +import { registerEmployeesTools } from './employees-tools.js'; +import { registerDispatchTools } from './dispatch-tools.js'; +import { registerTagsTools } from './tags-tools.js'; +import { registerNotificationsTools } from './notifications-tools.js'; +import { registerReviewsTools } from './reviews-tools.js'; +import { registerReportingTools } from './reporting-tools.js'; + +export function registerAllTools(client: HousecallProClient) { + return { + ...registerJobsTools(client), + ...registerCustomersTools(client), + ...registerEstimatesTools(client), + ...registerInvoicesTools(client), + ...registerEmployeesTools(client), + ...registerDispatchTools(client), + ...registerTagsTools(client), + ...registerNotificationsTools(client), + ...registerReviewsTools(client), + ...registerReportingTools(client), + }; +} diff --git a/servers/housecall-pro/src/tools/invoices-tools.ts b/servers/housecall-pro/src/tools/invoices-tools.ts new file mode 100644 index 0000000..0a2ec6a --- /dev/null +++ b/servers/housecall-pro/src/tools/invoices-tools.ts @@ -0,0 +1,187 @@ +/** + * Invoice Management Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { Invoice, CreateInvoiceRequest, Payment } from '../types/index.js'; + +export function registerInvoicesTools(client: HousecallProClient) { + return { + list_invoices: { + description: 'List invoices with optional filters', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Filter by customer ID', + }, + job_id: { + type: 'string', + description: 'Filter by job ID', + }, + status: { + type: 'string', + enum: ['draft', 'sent', 'viewed', 'partial', 'paid', 'overdue'], + description: 'Filter by status', + }, + start_date: { + type: 'string', + description: 'Filter invoices created on or after this date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'Filter invoices created on or before this date (ISO 8601)', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + }, + handler: async (params: any) => { + const invoices = await client.listInvoices(params); + return { invoices, count: invoices.length }; + }, + }, + + get_invoice: { + description: 'Get detailed information about a specific invoice', + parameters: { + type: 'object', + properties: { + invoice_id: { + type: 'string', + description: 'Invoice ID', + }, + }, + required: ['invoice_id'], + }, + handler: async (params: { invoice_id: string }) => { + const invoice = await client.getInvoice(params.invoice_id); + return invoice; + }, + }, + + create_invoice: { + description: 'Create a new invoice', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + line_items: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + quantity: { type: 'number' }, + unit_price: { type: 'number' }, + }, + required: ['name', 'quantity', 'unit_price'], + }, + description: 'Array of line items', + }, + due_date: { + type: 'string', + description: 'Due date (ISO 8601)', + }, + send_immediately: { + type: 'boolean', + description: 'Send the invoice immediately after creation', + }, + }, + required: ['job_id'], + }, + handler: async (params: CreateInvoiceRequest) => { + const invoice = await client.createInvoice(params); + return invoice; + }, + }, + + send_invoice: { + description: 'Send an invoice to the customer', + parameters: { + type: 'object', + properties: { + invoice_id: { + type: 'string', + description: 'Invoice ID', + }, + email: { + type: 'string', + description: 'Override customer email', + }, + message: { + type: 'string', + description: 'Custom message to include', + }, + }, + required: ['invoice_id'], + }, + handler: async (params: any) => { + const { invoice_id, ...options } = params; + const result = await client.sendInvoice(invoice_id, options); + return result; + }, + }, + + mark_invoice_paid: { + description: 'Record a payment for an invoice', + parameters: { + type: 'object', + properties: { + invoice_id: { + type: 'string', + description: 'Invoice ID', + }, + amount: { + type: 'number', + description: 'Payment amount', + }, + method: { + type: 'string', + enum: ['cash', 'check', 'card', 'ach', 'other'], + description: 'Payment method', + }, + reference: { + type: 'string', + description: 'Payment reference (check number, transaction ID, etc.)', + }, + }, + required: ['invoice_id', 'amount', 'method'], + }, + handler: async (params: any) => { + const { invoice_id, ...paymentData } = params; + const payment = await client.markInvoicePaid(invoice_id, paymentData); + return payment; + }, + }, + + list_invoice_payments: { + description: 'List all payments for an invoice', + parameters: { + type: 'object', + properties: { + invoice_id: { + type: 'string', + description: 'Invoice ID', + }, + }, + required: ['invoice_id'], + }, + handler: async (params: { invoice_id: string }) => { + const payments = await client.listInvoicePayments(params.invoice_id); + return payments; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/tools/jobs-tools.ts b/servers/housecall-pro/src/tools/jobs-tools.ts new file mode 100644 index 0000000..221d00f --- /dev/null +++ b/servers/housecall-pro/src/tools/jobs-tools.ts @@ -0,0 +1,346 @@ +/** + * Job Management Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { Job, CreateJobRequest } from '../types/index.js'; + +export function registerJobsTools(client: HousecallProClient) { + return { + list_jobs: { + description: 'List jobs with optional filters', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Filter by customer ID', + }, + work_status: { + type: 'string', + enum: ['scheduled', 'on_my_way', 'working', 'completed', 'cancelled'], + description: 'Filter by work status', + }, + invoice_status: { + type: 'string', + enum: ['not_invoiced', 'invoiced', 'paid', 'partial'], + description: 'Filter by invoice status', + }, + start_date: { + type: 'string', + description: 'Filter jobs scheduled on or after this date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'Filter jobs scheduled on or before this date (ISO 8601)', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Filter by tags', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + }, + handler: async (params: any) => { + const jobs = await client.listJobs(params); + return { jobs, count: jobs.length }; + }, + }, + + get_job: { + description: 'Get detailed information about a specific job', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + }, + required: ['job_id'], + }, + handler: async (params: { job_id: string }) => { + const job = await client.getJob(params.job_id); + return job; + }, + }, + + create_job: { + description: 'Create a new job', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Customer ID', + }, + address_id: { + type: 'string', + description: 'Service address ID', + }, + description: { + type: 'string', + description: 'Job description', + }, + notes: { + type: 'string', + description: 'Internal notes', + }, + schedule_start: { + type: 'string', + description: 'Scheduled start time (ISO 8601)', + }, + schedule_end: { + type: 'string', + description: 'Scheduled end time (ISO 8601)', + }, + arrival_window: { + type: 'string', + description: 'Arrival window (e.g., "8am-12pm")', + }, + assigned_employees: { + type: 'array', + items: { type: 'string' }, + description: 'Array of employee IDs to assign', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Array of tag IDs', + }, + }, + required: ['customer_id'], + }, + handler: async (params: any) => { + const jobData: CreateJobRequest = { + customer_id: params.customer_id, + address_id: params.address_id, + description: params.description, + notes: params.notes, + assigned_employees: params.assigned_employees, + tags: params.tags, + }; + + if (params.schedule_start && params.schedule_end) { + jobData.schedule = { + start: params.schedule_start, + end: params.schedule_end, + arrival_window: params.arrival_window, + }; + } + + const job = await client.createJob(jobData); + return job; + }, + }, + + update_job: { + description: 'Update an existing job', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + description: { + type: 'string', + description: 'Job description', + }, + notes: { + type: 'string', + description: 'Internal notes', + }, + work_status: { + type: 'string', + enum: ['scheduled', 'on_my_way', 'working', 'completed', 'cancelled'], + description: 'Work status', + }, + assigned_employees: { + type: 'array', + items: { type: 'string' }, + description: 'Array of employee IDs to assign', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Array of tag IDs', + }, + }, + required: ['job_id'], + }, + handler: async (params: any) => { + const { job_id, ...updateData } = params; + const job = await client.updateJob(job_id, updateData); + return job; + }, + }, + + complete_job: { + description: 'Mark a job as completed', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + }, + required: ['job_id'], + }, + handler: async (params: { job_id: string }) => { + const result = await client.completeJob(params.job_id); + return result; + }, + }, + + cancel_job: { + description: 'Cancel a job', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + reason: { + type: 'string', + description: 'Cancellation reason', + }, + }, + required: ['job_id'], + }, + handler: async (params: { job_id: string; reason?: string }) => { + const result = await client.cancelJob(params.job_id, params.reason); + return result; + }, + }, + + list_job_line_items: { + description: 'List line items for a job', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + }, + required: ['job_id'], + }, + handler: async (params: { job_id: string }) => { + const lineItems = await client.listJobLineItems(params.job_id); + return lineItems; + }, + }, + + add_job_line_item: { + description: 'Add a line item to a job', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + name: { + type: 'string', + description: 'Line item name', + }, + description: { + type: 'string', + description: 'Line item description', + }, + quantity: { + type: 'number', + description: 'Quantity', + }, + unit_price: { + type: 'number', + description: 'Unit price', + }, + unit: { + type: 'string', + description: 'Unit of measurement (e.g., "hours", "each")', + }, + }, + required: ['job_id', 'name', 'quantity', 'unit_price'], + }, + handler: async (params: any) => { + const { job_id, ...lineItemData } = params; + const lineItem = await client.addJobLineItem(job_id, lineItemData); + return lineItem; + }, + }, + + schedule_job: { + description: 'Schedule a job (set initial schedule)', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + start: { + type: 'string', + description: 'Scheduled start time (ISO 8601)', + }, + end: { + type: 'string', + description: 'Scheduled end time (ISO 8601)', + }, + arrival_window: { + type: 'string', + description: 'Arrival window (e.g., "8am-12pm")', + }, + }, + required: ['job_id', 'start', 'end'], + }, + handler: async (params: any) => { + const { job_id, ...schedule } = params; + const result = await client.scheduleJob(job_id, schedule); + return result; + }, + }, + + reschedule_job: { + description: 'Reschedule an existing job', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + start: { + type: 'string', + description: 'New scheduled start time (ISO 8601)', + }, + end: { + type: 'string', + description: 'New scheduled end time (ISO 8601)', + }, + arrival_window: { + type: 'string', + description: 'New arrival window (e.g., "8am-12pm")', + }, + }, + required: ['job_id', 'start', 'end'], + }, + handler: async (params: any) => { + const { job_id, ...schedule } = params; + const result = await client.rescheduleJob(job_id, schedule); + return result; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/tools/notifications-tools.ts b/servers/housecall-pro/src/tools/notifications-tools.ts new file mode 100644 index 0000000..6abecc4 --- /dev/null +++ b/servers/housecall-pro/src/tools/notifications-tools.ts @@ -0,0 +1,90 @@ +/** + * Notification Management Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { Notification } from '../types/index.js'; + +export function registerNotificationsTools(client: HousecallProClient) { + return { + list_notifications: { + description: 'List notifications with optional filters', + parameters: { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['sms', 'email', 'push'], + description: 'Filter by notification type', + }, + status: { + type: 'string', + enum: ['queued', 'sent', 'delivered', 'failed'], + description: 'Filter by status', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + }, + handler: async (params: any) => { + const notifications = await client.listNotifications(params); + return { notifications, count: notifications.length }; + }, + }, + + send_notification: { + description: 'Send a notification to a customer', + parameters: { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['sms', 'email'], + description: 'Notification type', + }, + recipient: { + type: 'string', + description: 'Recipient (phone number for SMS, email address for email)', + }, + subject: { + type: 'string', + description: 'Subject (email only)', + }, + message: { + type: 'string', + description: 'Message content', + }, + }, + required: ['type', 'recipient', 'message'], + }, + handler: async (params: any) => { + const notification = await client.sendNotification(params); + return notification; + }, + }, + + mark_notification_read: { + description: 'Mark a notification as read', + parameters: { + type: 'object', + properties: { + notification_id: { + type: 'string', + description: 'Notification ID', + }, + }, + required: ['notification_id'], + }, + handler: async (params: { notification_id: string }) => { + const result = await client.markNotificationRead(params.notification_id); + return result; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/tools/reporting-tools.ts b/servers/housecall-pro/src/tools/reporting-tools.ts new file mode 100644 index 0000000..adb751a --- /dev/null +++ b/servers/housecall-pro/src/tools/reporting-tools.ts @@ -0,0 +1,85 @@ +/** + * Reporting and Analytics Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { RevenueReport, JobCompletionReport, EmployeePerformanceReport } from '../types/index.js'; + +export function registerReportingTools(client: HousecallProClient) { + return { + get_revenue_report: { + description: 'Get revenue report for a date range', + parameters: { + type: 'object', + properties: { + start_date: { + type: 'string', + description: 'Start date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date (ISO 8601)', + }, + group_by: { + type: 'string', + enum: ['day', 'week', 'month'], + description: 'Group results by time period', + }, + }, + required: ['start_date', 'end_date'], + }, + handler: async (params: any) => { + const report = await client.getRevenueReport(params); + return report; + }, + }, + + get_job_completion_report: { + description: 'Get job completion statistics for a date range', + parameters: { + type: 'object', + properties: { + start_date: { + type: 'string', + description: 'Start date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date (ISO 8601)', + }, + }, + required: ['start_date', 'end_date'], + }, + handler: async (params: any) => { + const report = await client.getJobCompletionReport(params); + return report; + }, + }, + + get_employee_performance_report: { + description: 'Get employee performance metrics for a date range', + parameters: { + type: 'object', + properties: { + employee_id: { + type: 'string', + description: 'Employee ID (optional - leave blank for all employees)', + }, + start_date: { + type: 'string', + description: 'Start date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date (ISO 8601)', + }, + }, + required: ['start_date', 'end_date'], + }, + handler: async (params: any) => { + const report = await client.getEmployeePerformanceReport(params); + return report; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/tools/reviews-tools.ts b/servers/housecall-pro/src/tools/reviews-tools.ts new file mode 100644 index 0000000..e78ffee --- /dev/null +++ b/servers/housecall-pro/src/tools/reviews-tools.ts @@ -0,0 +1,89 @@ +/** + * Review Management Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { Review } from '../types/index.js'; + +export function registerReviewsTools(client: HousecallProClient) { + return { + list_reviews: { + description: 'List reviews with optional filters', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Filter by customer ID', + }, + job_id: { + type: 'string', + description: 'Filter by job ID', + }, + rating: { + type: 'number', + description: 'Filter by rating (1-5)', + }, + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + }, + handler: async (params: any) => { + const reviews = await client.listReviews(params); + return { reviews, count: reviews.length }; + }, + }, + + get_review: { + description: 'Get detailed information about a specific review', + parameters: { + type: 'object', + properties: { + review_id: { + type: 'string', + description: 'Review ID', + }, + }, + required: ['review_id'], + }, + handler: async (params: { review_id: string }) => { + const review = await client.getReview(params.review_id); + return review; + }, + }, + + request_review: { + description: 'Request a review from a customer for a completed job', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + method: { + type: 'string', + enum: ['sms', 'email'], + description: 'Review request method', + }, + message: { + type: 'string', + description: 'Custom message to include with the review request', + }, + }, + required: ['job_id'], + }, + handler: async (params: any) => { + const { job_id, ...options } = params; + const result = await client.requestReview(job_id, options); + return result; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/tools/tags-tools.ts b/servers/housecall-pro/src/tools/tags-tools.ts new file mode 100644 index 0000000..8b60119 --- /dev/null +++ b/servers/housecall-pro/src/tools/tags-tools.ts @@ -0,0 +1,115 @@ +/** + * Tag Management Tools + */ + +import { HousecallProClient } from '../clients/housecall-pro.js'; +import { Tag } from '../types/index.js'; + +export function registerTagsTools(client: HousecallProClient) { + return { + list_tags: { + description: 'List all tags', + parameters: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number (default: 1)', + }, + page_size: { + type: 'number', + description: 'Items per page (default: 50)', + }, + }, + }, + handler: async (params: any) => { + const tags = await client.listTags(params); + return { tags, count: tags.length }; + }, + }, + + create_tag: { + description: 'Create a new tag', + parameters: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Tag name', + }, + color: { + type: 'string', + description: 'Tag color (hex code)', + }, + }, + required: ['name'], + }, + handler: async (params: { name: string; color?: string }) => { + const tag = await client.createTag(params); + return tag; + }, + }, + + delete_tag: { + description: 'Delete a tag', + parameters: { + type: 'object', + properties: { + tag_id: { + type: 'string', + description: 'Tag ID', + }, + }, + required: ['tag_id'], + }, + handler: async (params: { tag_id: string }) => { + const result = await client.deleteTag(params.tag_id); + return result; + }, + }, + + add_tag_to_job: { + description: 'Add a tag to a job', + parameters: { + type: 'object', + properties: { + job_id: { + type: 'string', + description: 'Job ID', + }, + tag_id: { + type: 'string', + description: 'Tag ID', + }, + }, + required: ['job_id', 'tag_id'], + }, + handler: async (params: { job_id: string; tag_id: string }) => { + const result = await client.addTagToJob(params.job_id, params.tag_id); + return result; + }, + }, + + add_tag_to_customer: { + description: 'Add a tag to a customer', + parameters: { + type: 'object', + properties: { + customer_id: { + type: 'string', + description: 'Customer ID', + }, + tag_id: { + type: 'string', + description: 'Tag ID', + }, + }, + required: ['customer_id', 'tag_id'], + }, + handler: async (params: { customer_id: string; tag_id: string }) => { + const result = await client.addTagToCustomer(params.customer_id, params.tag_id); + return result; + }, + }, + }; +} diff --git a/servers/housecall-pro/src/ui/react-app/customer-detail.tsx b/servers/housecall-pro/src/ui/react-app/customer-detail.tsx new file mode 100644 index 0000000..96b8ea6 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/customer-detail.tsx @@ -0,0 +1,105 @@ +/** + * Customer Detail - Customer profile view + * MCP App for Housecall Pro + */ + +import React, { useState, useEffect } from 'react'; + +interface CustomerDetailProps { + api: any; + customerId: string; +} + +export function CustomerDetail({ api, customerId }: CustomerDetailProps) { + const [customer, setCustomer] = useState(null); + const [addresses, setAddresses] = useState([]); + const [jobs, setJobs] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadCustomer(); + }, [customerId]); + + const loadCustomer = async () => { + setLoading(true); + try { + const customerResult = await api.call('get_customer', { customer_id: customerId }); + setCustomer(JSON.parse(customerResult)); + + const addressesResult = await api.call('list_customer_addresses', { customer_id: customerId }); + setAddresses(JSON.parse(addressesResult)); + + const jobsResult = await api.call('list_jobs', { customer_id: customerId, page_size: 10 }); + setJobs(JSON.parse(jobsResult).jobs); + } catch (error) { + console.error('Failed to load customer:', error); + } finally { + setLoading(false); + } + }; + + if (loading) return
Loading customer...
; + if (!customer) return
Customer not found
; + + return ( +
+

{customer.first_name} {customer.last_name}

+ +
+
+

Contact Information

+

Email: {customer.email || 'N/A'}

+

Mobile: {customer.mobile_number || 'N/A'}

+

Home: {customer.home_number || 'N/A'}

+

Work: {customer.work_number || 'N/A'}

+ {customer.company &&

Company: {customer.company}

} +
+ +
+

Details

+

Lead Source: {customer.lead_source || 'N/A'}

+

Notifications: {customer.notifications_enabled ? 'Enabled' : 'Disabled'}

+ {customer.tags && customer.tags.length > 0 && ( +

Tags: {customer.tags.join(', ')}

+ )} +
+
+ +
+

Addresses

+ {addresses.map((addr, idx) => ( +
+

{addr.type || 'Address'}:

+

{addr.street}

+ {addr.street_line_2 &&

{addr.street_line_2}

} +

{addr.city}, {addr.state} {addr.zip}

+
+ ))} +
+ +
+

Recent Jobs

+ + + + + + + + + + + {jobs.map((job) => ( + + + + + + + ))} + +
IDDescriptionStatusDate
{job.id}{job.description || 'N/A'}{job.work_status}{job.schedule?.start ? new Date(job.schedule.start).toLocaleDateString() : '-'}
+
+
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/customer-grid.tsx b/servers/housecall-pro/src/ui/react-app/customer-grid.tsx new file mode 100644 index 0000000..faa3c29 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/customer-grid.tsx @@ -0,0 +1,77 @@ +/** + * Customer Grid - Customer list/grid view + * MCP App for Housecall Pro + */ + +import React, { useState, useEffect } from 'react'; + +interface CustomerGridProps { + api: any; +} + +export function CustomerGrid({ api }: CustomerGridProps) { + const [customers, setCustomers] = useState([]); + const [search, setSearch] = useState(''); + const [loading, setLoading] = useState(false); + + useEffect(() => { + loadCustomers(); + }, [search]); + + const loadCustomers = async () => { + setLoading(true); + try { + const params = search ? { search } : {}; + const result = await api.call('list_customers', params); + setCustomers(JSON.parse(result).customers); + } catch (error) { + console.error('Failed to load customers:', error); + } finally { + setLoading(false); + } + }; + + return ( +
+

Customers

+ +
+ setSearch(e.target.value)} + /> +
+ + {loading ? ( +
Loading customers...
+ ) : ( + + + + + + + + + + + + + {customers.map((customer) => ( + + + + + + + + + ))} + +
NameEmailPhoneCompanyLead SourceTags
{customer.first_name} {customer.last_name}{customer.email || '-'}{customer.mobile_number || customer.home_number || '-'}{customer.company || '-'}{customer.lead_source || '-'}{customer.tags?.join(', ') || '-'}
+ )} +
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/dispatch-board.tsx b/servers/housecall-pro/src/ui/react-app/dispatch-board.tsx new file mode 100644 index 0000000..42a506b --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/dispatch-board.tsx @@ -0,0 +1,34 @@ +/** + * Dispatch Board - Visual dispatch/scheduling board + */ + +import React, { useState, useEffect } from 'react'; + +export function DispatchBoard({ api }: { api: any }) { + const [board, setBoard] = useState(null); + const [date, setDate] = useState(new Date().toISOString().split('T')[0]); + + useEffect(() => { + loadBoard(); + }, [date]); + + const loadBoard = async () => { + const result = await api.call('get_dispatch_board', { date }); + setBoard(JSON.parse(result)); + }; + + return ( +
+

Dispatch Board

+ setDate(e.target.value)} /> + {board ? ( +
+ {/* Board visualization would go here */} +
{JSON.stringify(board, null, 2)}
+
+ ) : ( +
Loading...
+ )} +
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/employee-performance.tsx b/servers/housecall-pro/src/ui/react-app/employee-performance.tsx new file mode 100644 index 0000000..7163575 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/employee-performance.tsx @@ -0,0 +1,53 @@ +/** + * Employee Performance - Performance metrics dashboard + */ + +import React, { useState, useEffect } from 'react'; + +export function EmployeePerformance({ api }: { api: any }) { + const [report, setReport] = useState(null); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + + useEffect(() => { + if (startDate && endDate) loadReport(); + }, [startDate, endDate]); + + const loadReport = async () => { + const result = await api.call('get_employee_performance_report', { + start_date: startDate, + end_date: endDate, + }); + setReport(JSON.parse(result)); + }; + + return ( +
+

Employee Performance

+
+ setStartDate(e.target.value)} /> + setEndDate(e.target.value)} /> +
+ {report && ( +
+
+

Jobs Completed

+

{report.jobs_completed}

+
+
+

Total Revenue

+

${report.total_revenue?.toFixed(2)}

+
+
+

Hours Worked

+

{report.hours_worked}

+
+
+

Average Rating

+

{report.average_rating?.toFixed(1)}

+
+
+ )} +
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/employee-schedule.tsx b/servers/housecall-pro/src/ui/react-app/employee-schedule.tsx new file mode 100644 index 0000000..66c38b1 --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/employee-schedule.tsx @@ -0,0 +1,35 @@ +/** + * Employee Schedule - Calendar view of employee schedules + */ + +import React, { useState, useEffect } from 'react'; + +export function EmployeeSchedule({ api, employeeId }: { api: any; employeeId: string }) { + const [schedule, setSchedule] = useState(null); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + + useEffect(() => { + if (startDate && endDate) loadSchedule(); + }, [employeeId, startDate, endDate]); + + const loadSchedule = async () => { + const result = await api.call('get_employee_schedule', { + employee_id: employeeId, + start_date: startDate, + end_date: endDate, + }); + setSchedule(JSON.parse(result)); + }; + + return ( +
+

Employee Schedule

+
+ setStartDate(e.target.value)} /> + setEndDate(e.target.value)} /> +
+ {schedule &&
{JSON.stringify(schedule, null, 2)}
} +
+ ); +} diff --git a/servers/housecall-pro/src/ui/react-app/estimate-builder.tsx b/servers/housecall-pro/src/ui/react-app/estimate-builder.tsx new file mode 100644 index 0000000..b35b3ed --- /dev/null +++ b/servers/housecall-pro/src/ui/react-app/estimate-builder.tsx @@ -0,0 +1,124 @@ +/** + * Estimate Builder - Interactive estimate creation + * MCP App for Housecall Pro + */ + +import React, { useState } from 'react'; + +interface EstimateBuilderProps { + api: any; +} + +export function EstimateBuilder({ api }: EstimateBuilderProps) { + const [customerId, setCustomerId] = useState(''); + const [lineItems, setLineItems] = useState([]); + const [notes, setNotes] = useState(''); + const [validUntil, setValidUntil] = useState(''); + + const addLineItem = () => { + setLineItems([...lineItems, { name: '', description: '', quantity: 1, unit_price: 0 }]); + }; + + const updateLineItem = (index: number, field: string, value: any) => { + const updated = [...lineItems]; + updated[index][field] = value; + setLineItems(updated); + }; + + const removeLineItem = (index: number) => { + setLineItems(lineItems.filter((_, i) => i !== index)); + }; + + const calculateTotal = () => { + return lineItems.reduce((sum, item) => sum + (item.quantity * item.unit_price), 0); + }; + + const handleSubmit = async () => { + try { + await api.call('create_estimate', { + customer_id: customerId, + line_items: lineItems, + notes, + valid_until: validUntil, + }); + alert('Estimate created successfully!'); + } catch (error) { + alert('Failed to create estimate'); + } + }; + + return ( +
+

Create Estimate

+ +
+ + setCustomerId(e.target.value)} + placeholder="Enter customer ID" + /> +
+ +
+

Line Items

+ {lineItems.map((item, index) => ( +
+ updateLineItem(index, 'name', e.target.value)} + /> + updateLineItem(index, 'description', e.target.value)} + /> + updateLineItem(index, 'quantity', parseFloat(e.target.value))} + /> + updateLineItem(index, 'unit_price', parseFloat(e.target.value))} + /> + ${(item.quantity * item.unit_price).toFixed(2)} + +
+ ))} + +
+ +
+

Total: ${calculateTotal().toFixed(2)}

+
+ +
+ +