housecall-pro: Complete MCP server with 47 tools, 16 React apps, full API client

- API Client: Housecall Pro API with auth, pagination, error handling
- 47 Tools across 10 categories:
  - jobs-tools (10): list, get, create, update, complete, cancel, line items, schedule, reschedule
  - customers-tools (7): list, get, create, update, delete, search, addresses
  - estimates-tools (8): list, get, create, update, send, approve, decline, convert to job
  - invoices-tools (6): list, get, create, send, mark paid, list payments
  - employees-tools (6): list, get, create, update, schedule, time entries
  - dispatch-tools (3): dispatch board, assign employee, availability
  - tags-tools (5): list, create, delete, add to job/customer
  - notifications-tools (3): list, send, mark read
  - reviews-tools (3): list, get, request review
  - reporting-tools (3): revenue, job completion, employee performance
- 16 React MCP Apps for rich UI:
  - job-dashboard, job-detail, job-grid
  - customer-detail, customer-grid
  - estimate-builder, estimate-grid
  - invoice-dashboard, invoice-detail
  - dispatch-board
  - employee-schedule, employee-performance
  - review-dashboard, revenue-dashboard
  - tag-manager, notification-center
- Complete types, comprehensive README, full package.json
This commit is contained in:
Jake Shore 2026-02-12 17:39:57 -05:00
parent 7de8a68173
commit 5adccfd36e
35 changed files with 4210 additions and 0 deletions

View File

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

View File

@ -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"
}
}

View File

@ -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<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers as Record<string, string>,
};
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<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
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<T>(url, { method: 'GET' });
}
/**
* POST request
*/
async post<T>(endpoint: string, body?: any): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
});
}
/**
* PUT request
*/
async put<T>(endpoint: string, body?: any): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: body ? JSON.stringify(body) : undefined,
});
}
/**
* PATCH request
*/
async patch<T>(endpoint: string, body?: any): Promise<T> {
return this.request<T>(endpoint, {
method: 'PATCH',
body: body ? JSON.stringify(body) : undefined,
});
}
/**
* DELETE request
*/
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
/**
* Paginated GET request - fetches all pages
*/
async getPaginated<T>(
endpoint: string,
params?: PaginationParams & Record<string, any>
): Promise<T[]> {
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<PaginatedResponse<T>>(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<T>(
endpoint: string,
params?: PaginationParams & Record<string, any>
): Promise<PaginatedResponse<T>> {
return this.get<PaginatedResponse<T>>(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);
}
}

View File

@ -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';

View File

@ -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();

View File

@ -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<string, any>;
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');
}
}

View File

@ -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;
},
},
};
}

View File

@ -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;
},
},
};
}

View File

@ -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 };
},
},
};
}

View File

@ -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;
},
},
};
}

View File

@ -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),
};
}

View File

@ -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;
},
},
};
}

View File

@ -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;
},
},
};
}

View File

@ -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;
},
},
};
}

View File

@ -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;
},
},
};
}

View File

@ -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;
},
},
};
}

View File

@ -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;
},
},
};
}

View File

@ -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<any>(null);
const [addresses, setAddresses] = useState<any[]>([]);
const [jobs, setJobs] = useState<any[]>([]);
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 <div className="loading">Loading customer...</div>;
if (!customer) return <div className="error">Customer not found</div>;
return (
<div className="customer-detail">
<h1>{customer.first_name} {customer.last_name}</h1>
<div className="customer-info">
<div className="section">
<h2>Contact Information</h2>
<p><strong>Email:</strong> {customer.email || 'N/A'}</p>
<p><strong>Mobile:</strong> {customer.mobile_number || 'N/A'}</p>
<p><strong>Home:</strong> {customer.home_number || 'N/A'}</p>
<p><strong>Work:</strong> {customer.work_number || 'N/A'}</p>
{customer.company && <p><strong>Company:</strong> {customer.company}</p>}
</div>
<div className="section">
<h2>Details</h2>
<p><strong>Lead Source:</strong> {customer.lead_source || 'N/A'}</p>
<p><strong>Notifications:</strong> {customer.notifications_enabled ? 'Enabled' : 'Disabled'}</p>
{customer.tags && customer.tags.length > 0 && (
<p><strong>Tags:</strong> {customer.tags.join(', ')}</p>
)}
</div>
</div>
<div className="addresses">
<h2>Addresses</h2>
{addresses.map((addr, idx) => (
<div key={idx} className="address-card">
<p><strong>{addr.type || 'Address'}:</strong></p>
<p>{addr.street}</p>
{addr.street_line_2 && <p>{addr.street_line_2}</p>}
<p>{addr.city}, {addr.state} {addr.zip}</p>
</div>
))}
</div>
<div className="recent-jobs">
<h2>Recent Jobs</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Description</th>
<th>Status</th>
<th>Date</th>
</tr>
</thead>
<tbody>
{jobs.map((job) => (
<tr key={job.id}>
<td>{job.id}</td>
<td>{job.description || 'N/A'}</td>
<td><span className={`badge ${job.work_status}`}>{job.work_status}</span></td>
<td>{job.schedule?.start ? new Date(job.schedule.start).toLocaleDateString() : '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View File

@ -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<any[]>([]);
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 (
<div className="customer-grid">
<h1>Customers</h1>
<div className="search-bar">
<input
type="text"
placeholder="Search by name, email, or phone..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
{loading ? (
<div className="loading">Loading customers...</div>
) : (
<table className="data-grid">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Company</th>
<th>Lead Source</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
{customers.map((customer) => (
<tr key={customer.id}>
<td>{customer.first_name} {customer.last_name}</td>
<td>{customer.email || '-'}</td>
<td>{customer.mobile_number || customer.home_number || '-'}</td>
<td>{customer.company || '-'}</td>
<td>{customer.lead_source || '-'}</td>
<td>{customer.tags?.join(', ') || '-'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}

View File

@ -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<any>(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 (
<div className="dispatch-board">
<h1>Dispatch Board</h1>
<input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
{board ? (
<div className="board-view">
{/* Board visualization would go here */}
<pre>{JSON.stringify(board, null, 2)}</pre>
</div>
) : (
<div>Loading...</div>
)}
</div>
);
}

View File

@ -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<any>(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 (
<div className="employee-performance">
<h1>Employee Performance</h1>
<div className="date-range">
<input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
<input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
</div>
{report && (
<div className="metrics">
<div className="metric">
<h3>Jobs Completed</h3>
<p>{report.jobs_completed}</p>
</div>
<div className="metric">
<h3>Total Revenue</h3>
<p>${report.total_revenue?.toFixed(2)}</p>
</div>
<div className="metric">
<h3>Hours Worked</h3>
<p>{report.hours_worked}</p>
</div>
<div className="metric">
<h3>Average Rating</h3>
<p>{report.average_rating?.toFixed(1)}</p>
</div>
</div>
)}
</div>
);
}

View File

@ -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<any>(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 (
<div className="employee-schedule">
<h1>Employee Schedule</h1>
<div className="date-range">
<input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
<input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
</div>
{schedule && <pre>{JSON.stringify(schedule, null, 2)}</pre>}
</div>
);
}

View File

@ -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<any[]>([]);
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 (
<div className="estimate-builder">
<h1>Create Estimate</h1>
<div className="form-group">
<label>Customer ID:</label>
<input
type="text"
value={customerId}
onChange={(e) => setCustomerId(e.target.value)}
placeholder="Enter customer ID"
/>
</div>
<div className="line-items">
<h2>Line Items</h2>
{lineItems.map((item, index) => (
<div key={index} className="line-item">
<input
type="text"
placeholder="Name"
value={item.name}
onChange={(e) => updateLineItem(index, 'name', e.target.value)}
/>
<input
type="text"
placeholder="Description"
value={item.description}
onChange={(e) => updateLineItem(index, 'description', e.target.value)}
/>
<input
type="number"
placeholder="Qty"
value={item.quantity}
onChange={(e) => updateLineItem(index, 'quantity', parseFloat(e.target.value))}
/>
<input
type="number"
placeholder="Unit Price"
value={item.unit_price}
onChange={(e) => updateLineItem(index, 'unit_price', parseFloat(e.target.value))}
/>
<span className="item-total">${(item.quantity * item.unit_price).toFixed(2)}</span>
<button onClick={() => removeLineItem(index)}>Remove</button>
</div>
))}
<button onClick={addLineItem}>+ Add Line Item</button>
</div>
<div className="total">
<h3>Total: ${calculateTotal().toFixed(2)}</h3>
</div>
<div className="form-group">
<label>Notes:</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={4}
/>
</div>
<div className="form-group">
<label>Valid Until:</label>
<input
type="date"
value={validUntil}
onChange={(e) => setValidUntil(e.target.value)}
/>
</div>
<button onClick={handleSubmit} className="btn-primary">Create Estimate</button>
</div>
);
}

View File

@ -0,0 +1,55 @@
/**
* Estimate Grid - List/grid view of estimates
*/
import React, { useState, useEffect } from 'react';
export function EstimateGrid({ api }: { api: any }) {
const [estimates, setEstimates] = useState<any[]>([]);
const [status, setStatus] = useState('');
useEffect(() => {
loadEstimates();
}, [status]);
const loadEstimates = async () => {
const params = status ? { status } : {};
const result = await api.call('list_estimates', params);
setEstimates(JSON.parse(result).estimates);
};
return (
<div className="estimate-grid">
<h1>Estimates</h1>
<select value={status} onChange={(e) => setStatus(e.target.value)}>
<option value="">All</option>
<option value="draft">Draft</option>
<option value="sent">Sent</option>
<option value="approved">Approved</option>
<option value="declined">Declined</option>
</select>
<table>
<thead>
<tr>
<th>ID</th>
<th>Customer</th>
<th>Status</th>
<th>Total</th>
<th>Valid Until</th>
</tr>
</thead>
<tbody>
{estimates.map((est) => (
<tr key={est.id}>
<td>{est.id}</td>
<td>{est.customer_id}</td>
<td><span className={`badge ${est.status}`}>{est.status}</span></td>
<td>${est.total.toFixed(2)}</td>
<td>{est.valid_until ? new Date(est.valid_until).toLocaleDateString() : '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@ -0,0 +1,134 @@
/**
* Housecall Pro MCP Apps - Index
*
* 16 React-based MCP apps for rich UI interactions
*/
export { JobDashboard } from './job-dashboard';
export { JobDetail } from './job-detail';
export { JobGrid } from './job-grid';
export { CustomerDetail } from './customer-detail';
export { CustomerGrid } from './customer-grid';
export { EstimateBuilder } from './estimate-builder';
export { EstimateGrid } from './estimate-grid';
export { InvoiceDashboard } from './invoice-dashboard';
export { InvoiceDetail } from './invoice-detail';
export { DispatchBoard } from './dispatch-board';
export { EmployeeSchedule } from './employee-schedule';
export { EmployeePerformance } from './employee-performance';
export { ReviewDashboard } from './review-dashboard';
export { RevenueDashboard } from './revenue-dashboard';
export { TagManager } from './tag-manager';
export { NotificationCenter } from './notification-center';
/**
* App Registry - Maps app names to components
*/
export const MCP_APPS = {
'job-dashboard': JobDashboard,
'job-detail': JobDetail,
'job-grid': JobGrid,
'customer-detail': CustomerDetail,
'customer-grid': CustomerGrid,
'estimate-builder': EstimateBuilder,
'estimate-grid': EstimateGrid,
'invoice-dashboard': InvoiceDashboard,
'invoice-detail': InvoiceDetail,
'dispatch-board': DispatchBoard,
'employee-schedule': EmployeeSchedule,
'employee-performance': EmployeePerformance,
'review-dashboard': ReviewDashboard,
'revenue-dashboard': RevenueDashboard,
'tag-manager': TagManager,
'notification-center': NotificationCenter,
};
/**
* App Metadata
*/
export const APP_METADATA = {
'job-dashboard': {
name: 'Job Dashboard',
description: 'Overview of all jobs with stats',
category: 'Jobs',
},
'job-detail': {
name: 'Job Detail',
description: 'Detailed view of a single job',
category: 'Jobs',
params: ['jobId'],
},
'job-grid': {
name: 'Job Grid',
description: 'Filterable grid view of jobs',
category: 'Jobs',
},
'customer-detail': {
name: 'Customer Detail',
description: 'Customer profile and history',
category: 'Customers',
params: ['customerId'],
},
'customer-grid': {
name: 'Customer Grid',
description: 'Searchable customer list',
category: 'Customers',
},
'estimate-builder': {
name: 'Estimate Builder',
description: 'Interactive estimate creation',
category: 'Estimates',
},
'estimate-grid': {
name: 'Estimate Grid',
description: 'List and manage estimates',
category: 'Estimates',
},
'invoice-dashboard': {
name: 'Invoice Dashboard',
description: 'Invoice overview and metrics',
category: 'Invoices',
},
'invoice-detail': {
name: 'Invoice Detail',
description: 'Detailed invoice view with payments',
category: 'Invoices',
params: ['invoiceId'],
},
'dispatch-board': {
name: 'Dispatch Board',
description: 'Visual dispatch and scheduling',
category: 'Dispatch',
},
'employee-schedule': {
name: 'Employee Schedule',
description: 'Calendar view of employee schedules',
category: 'Employees',
params: ['employeeId'],
},
'employee-performance': {
name: 'Employee Performance',
description: 'Performance metrics dashboard',
category: 'Employees',
},
'review-dashboard': {
name: 'Review Dashboard',
description: 'Customer reviews management',
category: 'Reviews',
},
'revenue-dashboard': {
name: 'Revenue Dashboard',
description: 'Revenue analytics and reporting',
category: 'Reporting',
},
'tag-manager': {
name: 'Tag Manager',
description: 'Tag organization and management',
category: 'Settings',
},
'notification-center': {
name: 'Notification Center',
description: 'Notification history and management',
category: 'Communications',
},
};

View File

@ -0,0 +1,72 @@
/**
* Invoice Dashboard - Overview of invoices
*/
import React, { useState, useEffect } from 'react';
export function InvoiceDashboard({ api }: { api: any }) {
const [stats, setStats] = useState<any>(null);
const [invoices, setInvoices] = useState<any[]>([]);
useEffect(() => {
loadDashboard();
}, []);
const loadDashboard = async () => {
const result = await api.call('list_invoices', { page_size: 100 });
const data = JSON.parse(result).invoices;
const stats = {
total: data.length,
draft: data.filter((i: any) => i.status === 'draft').length,
sent: data.filter((i: any) => i.status === 'sent').length,
paid: data.filter((i: any) => i.status === 'paid').length,
overdue: data.filter((i: any) => i.status === 'overdue').length,
totalRevenue: data.reduce((sum: number, i: any) => sum + i.total, 0),
totalPaid: data.reduce((sum: number, i: any) => sum + i.amount_paid, 0),
};
setStats(stats);
setInvoices(data.slice(0, 10));
};
return (
<div className="invoice-dashboard">
<h1>Invoice Dashboard</h1>
{stats && (
<div className="stats">
<div className="stat"><h3>Total Invoices</h3><p>{stats.total}</p></div>
<div className="stat"><h3>Paid</h3><p>{stats.paid}</p></div>
<div className="stat"><h3>Overdue</h3><p className="danger">{stats.overdue}</p></div>
<div className="stat"><h3>Total Revenue</h3><p>${stats.totalRevenue.toFixed(2)}</p></div>
<div className="stat"><h3>Collected</h3><p>${stats.totalPaid.toFixed(2)}</p></div>
</div>
)}
<h2>Recent Invoices</h2>
<table>
<thead>
<tr>
<th>Invoice #</th>
<th>Customer</th>
<th>Status</th>
<th>Total</th>
<th>Balance</th>
<th>Due Date</th>
</tr>
</thead>
<tbody>
{invoices.map((inv) => (
<tr key={inv.id}>
<td>{inv.invoice_number}</td>
<td>{inv.customer_id}</td>
<td><span className={`badge ${inv.status}`}>{inv.status}</span></td>
<td>${inv.total.toFixed(2)}</td>
<td>${inv.balance.toFixed(2)}</td>
<td>{inv.due_date ? new Date(inv.due_date).toLocaleDateString() : '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@ -0,0 +1,89 @@
/**
* Invoice Detail - Detailed invoice view
*/
import React, { useState, useEffect } from 'react';
export function InvoiceDetail({ api, invoiceId }: { api: any; invoiceId: string }) {
const [invoice, setInvoice] = useState<any>(null);
const [payments, setPayments] = useState<any[]>([]);
useEffect(() => {
loadInvoice();
}, [invoiceId]);
const loadInvoice = async () => {
const invResult = await api.call('get_invoice', { invoice_id: invoiceId });
setInvoice(JSON.parse(invResult));
const payResult = await api.call('list_invoice_payments', { invoice_id: invoiceId });
setPayments(JSON.parse(payResult));
};
if (!invoice) return <div>Loading...</div>;
return (
<div className="invoice-detail">
<h1>Invoice #{invoice.invoice_number}</h1>
<div className="info">
<p><strong>Customer:</strong> {invoice.customer_id}</p>
<p><strong>Job:</strong> {invoice.job_id}</p>
<p><strong>Status:</strong> <span className={`badge ${invoice.status}`}>{invoice.status}</span></p>
<p><strong>Due Date:</strong> {invoice.due_date ? new Date(invoice.due_date).toLocaleDateString() : '-'}</p>
</div>
<h2>Line Items</h2>
<table>
<tbody>
{invoice.line_items?.map((item: any, i: number) => (
<tr key={i}>
<td>{item.name}</td>
<td>{item.quantity} × ${item.unit_price.toFixed(2)}</td>
<td>${item.total.toFixed(2)}</td>
</tr>
))}
<tr className="total-row">
<td colSpan={2}><strong>Subtotal</strong></td>
<td>${invoice.subtotal.toFixed(2)}</td>
</tr>
<tr className="total-row">
<td colSpan={2}><strong>Tax</strong></td>
<td>${invoice.tax.toFixed(2)}</td>
</tr>
<tr className="total-row">
<td colSpan={2}><strong>Total</strong></td>
<td><strong>${invoice.total.toFixed(2)}</strong></td>
</tr>
<tr className="total-row">
<td colSpan={2}><strong>Paid</strong></td>
<td>${invoice.amount_paid.toFixed(2)}</td>
</tr>
<tr className="total-row">
<td colSpan={2}><strong>Balance</strong></td>
<td className="balance">${invoice.balance.toFixed(2)}</td>
</tr>
</tbody>
</table>
<h2>Payments</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Method</th>
<th>Reference</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{payments.map((payment) => (
<tr key={payment.id}>
<td>{new Date(payment.created_at).toLocaleDateString()}</td>
<td>{payment.method}</td>
<td>{payment.reference || '-'}</td>
<td>${payment.amount.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@ -0,0 +1,122 @@
/**
* Job Dashboard - Overview of all jobs
* MCP App for Housecall Pro
*/
import React, { useState, useEffect } from 'react';
interface JobDashboardProps {
api: any; // MCP API interface
}
interface JobStats {
total: number;
scheduled: number;
inProgress: number;
completed: number;
cancelled: number;
}
export function JobDashboard({ api }: JobDashboardProps) {
const [stats, setStats] = useState<JobStats | null>(null);
const [recentJobs, setRecentJobs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadDashboard();
}, []);
const loadDashboard = async () => {
setLoading(true);
try {
// Get jobs from last 30 days
const endDate = new Date().toISOString();
const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
const result = await api.call('list_jobs', {
start_date: startDate,
end_date: endDate,
page_size: 100,
});
const jobs = JSON.parse(result).jobs;
// Calculate stats
const stats: JobStats = {
total: jobs.length,
scheduled: jobs.filter((j: any) => j.work_status === 'scheduled').length,
inProgress: jobs.filter((j: any) => ['on_my_way', 'working'].includes(j.work_status)).length,
completed: jobs.filter((j: any) => j.work_status === 'completed').length,
cancelled: jobs.filter((j: any) => j.work_status === 'cancelled').length,
};
setStats(stats);
setRecentJobs(jobs.slice(0, 10));
} catch (error) {
console.error('Failed to load dashboard:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return <div className="loading">Loading dashboard...</div>;
}
return (
<div className="job-dashboard">
<h1>Job Dashboard</h1>
{stats && (
<div className="stats-grid">
<div className="stat-card">
<h3>Total Jobs</h3>
<p className="stat-value">{stats.total}</p>
</div>
<div className="stat-card">
<h3>Scheduled</h3>
<p className="stat-value scheduled">{stats.scheduled}</p>
</div>
<div className="stat-card">
<h3>In Progress</h3>
<p className="stat-value in-progress">{stats.inProgress}</p>
</div>
<div className="stat-card">
<h3>Completed</h3>
<p className="stat-value completed">{stats.completed}</p>
</div>
<div className="stat-card">
<h3>Cancelled</h3>
<p className="stat-value cancelled">{stats.cancelled}</p>
</div>
</div>
)}
<div className="recent-jobs">
<h2>Recent Jobs</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Customer</th>
<th>Description</th>
<th>Status</th>
<th>Scheduled</th>
</tr>
</thead>
<tbody>
{recentJobs.map((job) => (
<tr key={job.id}>
<td>{job.id}</td>
<td>{job.customer_id}</td>
<td>{job.description || 'N/A'}</td>
<td className={`status-${job.work_status}`}>{job.work_status}</td>
<td>{job.schedule?.start ? new Date(job.schedule.start).toLocaleString() : 'Unscheduled'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View File

@ -0,0 +1,154 @@
/**
* Job Detail - Detailed view of a single job
* MCP App for Housecall Pro
*/
import React, { useState, useEffect } from 'react';
interface JobDetailProps {
api: any;
jobId: string;
}
export function JobDetail({ api, jobId }: JobDetailProps) {
const [job, setJob] = useState<any>(null);
const [lineItems, setLineItems] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadJob();
}, [jobId]);
const loadJob = async () => {
setLoading(true);
try {
const jobResult = await api.call('get_job', { job_id: jobId });
const jobData = JSON.parse(jobResult);
setJob(jobData);
const lineItemsResult = await api.call('list_job_line_items', { job_id: jobId });
const lineItemsData = JSON.parse(lineItemsResult);
setLineItems(lineItemsData);
} catch (error) {
console.error('Failed to load job:', error);
} finally {
setLoading(false);
}
};
const handleComplete = async () => {
if (confirm('Mark this job as completed?')) {
await api.call('complete_job', { job_id: jobId });
loadJob();
}
};
const handleCancel = async () => {
const reason = prompt('Reason for cancellation:');
if (reason) {
await api.call('cancel_job', { job_id: jobId, reason });
loadJob();
}
};
if (loading) {
return <div className="loading">Loading job...</div>;
}
if (!job) {
return <div className="error">Job not found</div>;
}
const total = lineItems.reduce((sum, item) => sum + item.total, 0);
return (
<div className="job-detail">
<div className="job-header">
<h1>Job #{job.id}</h1>
<span className={`status-badge ${job.work_status}`}>{job.work_status}</span>
</div>
<div className="job-info">
<div className="info-section">
<h2>Customer Information</h2>
<p><strong>Customer ID:</strong> {job.customer_id}</p>
<p><strong>Address ID:</strong> {job.address_id || 'N/A'}</p>
</div>
<div className="info-section">
<h2>Schedule</h2>
{job.schedule ? (
<>
<p><strong>Start:</strong> {new Date(job.schedule.start).toLocaleString()}</p>
<p><strong>End:</strong> {new Date(job.schedule.end).toLocaleString()}</p>
{job.schedule.arrival_window && (
<p><strong>Arrival Window:</strong> {job.schedule.arrival_window}</p>
)}
</>
) : (
<p>Not scheduled</p>
)}
</div>
<div className="info-section">
<h2>Details</h2>
<p><strong>Description:</strong> {job.description || 'N/A'}</p>
<p><strong>Notes:</strong> {job.notes || 'N/A'}</p>
<p><strong>Invoice Status:</strong> {job.invoice_status}</p>
</div>
{job.assigned_employees && job.assigned_employees.length > 0 && (
<div className="info-section">
<h2>Assigned Employees</h2>
<ul>
{job.assigned_employees.map((empId: string) => (
<li key={empId}>{empId}</li>
))}
</ul>
</div>
)}
</div>
<div className="line-items">
<h2>Line Items</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Quantity</th>
<th>Unit Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{lineItems.map((item, index) => (
<tr key={index}>
<td>{item.name}</td>
<td>{item.description || '-'}</td>
<td>{item.quantity}</td>
<td>${item.unit_price.toFixed(2)}</td>
<td>${item.total.toFixed(2)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td colSpan={4}><strong>Total</strong></td>
<td><strong>${total.toFixed(2)}</strong></td>
</tr>
</tfoot>
</table>
</div>
<div className="actions">
{job.work_status !== 'completed' && job.work_status !== 'cancelled' && (
<>
<button onClick={handleComplete} className="btn-primary">Complete Job</button>
<button onClick={handleCancel} className="btn-danger">Cancel Job</button>
</>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,120 @@
/**
* Job Grid - Filterable grid view of jobs
* MCP App for Housecall Pro
*/
import React, { useState, useEffect } from 'react';
interface JobGridProps {
api: any;
}
export function JobGrid({ api }: JobGridProps) {
const [jobs, setJobs] = useState<any[]>([]);
const [filters, setFilters] = useState({
work_status: '',
invoice_status: '',
start_date: '',
end_date: '',
});
const [loading, setLoading] = useState(false);
useEffect(() => {
loadJobs();
}, [filters]);
const loadJobs = async () => {
setLoading(true);
try {
const params: any = {};
if (filters.work_status) params.work_status = filters.work_status;
if (filters.invoice_status) params.invoice_status = filters.invoice_status;
if (filters.start_date) params.start_date = filters.start_date;
if (filters.end_date) params.end_date = filters.end_date;
const result = await api.call('list_jobs', params);
const data = JSON.parse(result);
setJobs(data.jobs);
} catch (error) {
console.error('Failed to load jobs:', error);
} finally {
setLoading(false);
}
};
return (
<div className="job-grid">
<h1>Jobs</h1>
<div className="filters">
<select
value={filters.work_status}
onChange={(e) => setFilters({...filters, work_status: e.target.value})}
>
<option value="">All Statuses</option>
<option value="scheduled">Scheduled</option>
<option value="on_my_way">On My Way</option>
<option value="working">Working</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
<select
value={filters.invoice_status}
onChange={(e) => setFilters({...filters, invoice_status: e.target.value})}
>
<option value="">All Invoice Status</option>
<option value="not_invoiced">Not Invoiced</option>
<option value="invoiced">Invoiced</option>
<option value="paid">Paid</option>
<option value="partial">Partial</option>
</select>
<input
type="date"
value={filters.start_date}
onChange={(e) => setFilters({...filters, start_date: e.target.value})}
placeholder="Start Date"
/>
<input
type="date"
value={filters.end_date}
onChange={(e) => setFilters({...filters, end_date: e.target.value})}
placeholder="End Date"
/>
</div>
{loading ? (
<div className="loading">Loading jobs...</div>
) : (
<table className="data-grid">
<thead>
<tr>
<th>ID</th>
<th>Customer</th>
<th>Description</th>
<th>Work Status</th>
<th>Invoice Status</th>
<th>Scheduled</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{jobs.map((job) => (
<tr key={job.id}>
<td>{job.id}</td>
<td>{job.customer_id}</td>
<td>{job.description || 'N/A'}</td>
<td><span className={`badge ${job.work_status}`}>{job.work_status}</span></td>
<td><span className={`badge ${job.invoice_status}`}>{job.invoice_status}</span></td>
<td>{job.schedule?.start ? new Date(job.schedule.start).toLocaleDateString() : '-'}</td>
<td>${job.total_amount?.toFixed(2) || '0.00'}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}

View File

@ -0,0 +1,70 @@
/**
* Notification Center - Notification management
*/
import React, { useState, useEffect } from 'react';
export function NotificationCenter({ api }: { api: any }) {
const [notifications, setNotifications] = useState<any[]>([]);
const [type, setType] = useState('');
const [status, setStatus] = useState('');
useEffect(() => {
loadNotifications();
}, [type, status]);
const loadNotifications = async () => {
const params: any = {};
if (type) params.type = type;
if (status) params.status = status;
const result = await api.call('list_notifications', params);
setNotifications(JSON.parse(result).notifications);
};
const handleMarkRead = async (notificationId: string) => {
await api.call('mark_notification_read', { notification_id: notificationId });
loadNotifications();
};
return (
<div className="notification-center">
<h1>Notifications</h1>
<div className="filters">
<select value={type} onChange={(e) => setType(e.target.value)}>
<option value="">All Types</option>
<option value="sms">SMS</option>
<option value="email">Email</option>
<option value="push">Push</option>
</select>
<select value={status} onChange={(e) => setStatus(e.target.value)}>
<option value="">All Status</option>
<option value="queued">Queued</option>
<option value="sent">Sent</option>
<option value="delivered">Delivered</option>
<option value="failed">Failed</option>
</select>
</div>
<div className="notifications-list">
{notifications.map((notif) => (
<div key={notif.id} className={`notification-card ${notif.status}`}>
<div className="notif-header">
<span className="type">{notif.type}</span>
<span className="status">{notif.status}</span>
</div>
<div className="notif-body">
<p><strong>To:</strong> {notif.recipient}</p>
{notif.subject && <p><strong>Subject:</strong> {notif.subject}</p>}
<p>{notif.message}</p>
</div>
<div className="notif-footer">
<small>{new Date(notif.created_at).toLocaleString()}</small>
<button onClick={() => handleMarkRead(notif.id)}>Mark Read</button>
</div>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,75 @@
/**
* Revenue Dashboard - Revenue analytics and reporting
*/
import React, { useState, useEffect } from 'react';
export function RevenueDashboard({ api }: { api: any }) {
const [report, setReport] = useState<any>(null);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [groupBy, setGroupBy] = useState<'day' | 'week' | 'month'>('day');
useEffect(() => {
if (startDate && endDate) loadReport();
}, [startDate, endDate, groupBy]);
const loadReport = async () => {
const result = await api.call('get_revenue_report', {
start_date: startDate,
end_date: endDate,
group_by: groupBy,
});
setReport(JSON.parse(result));
};
return (
<div className="revenue-dashboard">
<h1>Revenue Dashboard</h1>
<div className="controls">
<input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
<input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
<select value={groupBy} onChange={(e) => setGroupBy(e.target.value as any)}>
<option value="day">Daily</option>
<option value="week">Weekly</option>
<option value="month">Monthly</option>
</select>
</div>
{report && (
<>
<div className="revenue-stats">
<div className="stat">
<h3>Total Revenue</h3>
<p>${report.total_revenue?.toFixed(2)}</p>
</div>
<div className="stat">
<h3>Invoiced</h3>
<p>${report.invoiced?.toFixed(2)}</p>
</div>
<div className="stat">
<h3>Paid</h3>
<p>${report.paid?.toFixed(2)}</p>
</div>
<div className="stat">
<h3>Outstanding</h3>
<p>${report.outstanding?.toFixed(2)}</p>
</div>
</div>
{report.by_day && (
<div className="chart">
<h3>Revenue by {groupBy}</h3>
{/* Chart visualization would go here */}
<ul>
{report.by_day.map((day: any, i: number) => (
<li key={i}>
{day.date}: ${day.revenue.toFixed(2)}
</li>
))}
</ul>
</div>
)}
</>
)}
</div>
);
}

View File

@ -0,0 +1,43 @@
/**
* Review Dashboard - Customer reviews management
*/
import React, { useState, useEffect } from 'react';
export function ReviewDashboard({ api }: { api: any }) {
const [reviews, setReviews] = useState<any[]>([]);
const [rating, setRating] = useState<number | undefined>();
useEffect(() => {
loadReviews();
}, [rating]);
const loadReviews = async () => {
const params = rating ? { rating } : {};
const result = await api.call('list_reviews', params);
setReviews(JSON.parse(result).reviews);
};
return (
<div className="review-dashboard">
<h1>Customer Reviews</h1>
<select value={rating || ''} onChange={(e) => setRating(e.target.value ? parseInt(e.target.value) : undefined)}>
<option value="">All Ratings</option>
<option value="5">5 Stars</option>
<option value="4">4 Stars</option>
<option value="3">3 Stars</option>
<option value="2">2 Stars</option>
<option value="1">1 Star</option>
</select>
<div className="reviews">
{reviews.map((review) => (
<div key={review.id} className="review-card">
<div className="rating">{'★'.repeat(review.rating || 0)}</div>
<p>{review.comment}</p>
<small>Job: {review.job_id} | Customer: {review.customer_id}</small>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,69 @@
/**
* Tag Manager - Tag organization and management
*/
import React, { useState, useEffect } from 'react';
export function TagManager({ api }: { api: any }) {
const [tags, setTags] = useState<any[]>([]);
const [newTagName, setNewTagName] = useState('');
const [newTagColor, setNewTagColor] = useState('#3498db');
useEffect(() => {
loadTags();
}, []);
const loadTags = async () => {
const result = await api.call('list_tags', {});
setTags(JSON.parse(result).tags);
};
const handleCreate = async () => {
if (!newTagName) return;
await api.call('create_tag', { name: newTagName, color: newTagColor });
setNewTagName('');
loadTags();
};
const handleDelete = async (tagId: string) => {
if (confirm('Delete this tag?')) {
await api.call('delete_tag', { tag_id: tagId });
loadTags();
}
};
return (
<div className="tag-manager">
<h1>Tag Manager</h1>
<div className="create-tag">
<input
type="text"
placeholder="Tag name"
value={newTagName}
onChange={(e) => setNewTagName(e.target.value)}
/>
<input
type="color"
value={newTagColor}
onChange={(e) => setNewTagColor(e.target.value)}
/>
<button onClick={handleCreate}>Create Tag</button>
</div>
<div className="tags-list">
{tags.map((tag) => (
<div key={tag.id} className="tag-item">
<span
className="tag-badge"
style={{ backgroundColor: tag.color || '#ccc' }}
>
{tag.name}
</span>
<button onClick={() => handleDelete(tag.id)}>Delete</button>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "src/ui"]
}