ServiceTitan: Complete MCP server with 55 tools, 20 React apps, OAuth2, pagination

This commit is contained in:
Jake Shore 2026-02-12 17:39:34 -05:00
parent fafbdbe188
commit 7de8a68173
36 changed files with 2698 additions and 0 deletions

View File

@ -0,0 +1,10 @@
# ServiceTitan API Credentials (Required)
SERVICETITAN_CLIENT_ID=your_client_id_here
SERVICETITAN_CLIENT_SECRET=your_client_secret_here
SERVICETITAN_TENANT_ID=your_tenant_id_here
SERVICETITAN_APP_KEY=your_app_key_here
# Optional Configuration
SERVICETITAN_BASE_URL=https://api.servicetitan.io
PORT=3000
MODE=stdio # or "http" for web apps

7
servers/servicetitan/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules/
dist/
.env
.DS_Store
*.log
.vscode/
.idea/

View File

@ -0,0 +1,275 @@
# ServiceTitan MCP Server
Complete Model Context Protocol (MCP) server for ServiceTitan field service management platform.
## Features
### 🔧 **55 MCP Tools** across 10 categories:
#### Jobs Management (9 tools)
- `servicetitan_list_jobs` - List jobs with filters
- `servicetitan_get_job` - Get job details
- `servicetitan_create_job` - Create new job
- `servicetitan_update_job` - Update job
- `servicetitan_cancel_job` - Cancel job
- `servicetitan_list_job_appointments` - List job appointments
- `servicetitan_create_job_appointment` - Create appointment
- `servicetitan_reschedule_appointment` - Reschedule appointment
- `servicetitan_get_job_history` - Get job history
#### Customer Management (9 tools)
- `servicetitan_list_customers` - List customers
- `servicetitan_get_customer` - Get customer details
- `servicetitan_create_customer` - Create customer
- `servicetitan_update_customer` - Update customer
- `servicetitan_search_customers` - Search customers
- `servicetitan_list_customer_contacts` - List contacts
- `servicetitan_create_customer_contact` - Create contact
- `servicetitan_list_customer_locations` - List locations
- `servicetitan_create_customer_location` - Create location
#### Invoice Management (8 tools)
- `servicetitan_list_invoices` - List invoices
- `servicetitan_get_invoice` - Get invoice details
- `servicetitan_create_invoice` - Create invoice
- `servicetitan_update_invoice` - Update invoice
- `servicetitan_list_invoice_items` - List invoice items
- `servicetitan_add_invoice_item` - Add invoice item
- `servicetitan_list_invoice_payments` - List payments
- `servicetitan_add_invoice_payment` - Add payment
#### Estimates (6 tools)
- `servicetitan_list_estimates` - List estimates
- `servicetitan_get_estimate` - Get estimate
- `servicetitan_create_estimate` - Create estimate
- `servicetitan_update_estimate` - Update estimate
- `servicetitan_convert_estimate_to_job` - Convert to job
- `servicetitan_list_estimate_items` - List items
#### Technician Management (6 tools)
- `servicetitan_list_technicians` - List technicians
- `servicetitan_get_technician` - Get technician details
- `servicetitan_create_technician` - Create technician
- `servicetitan_update_technician` - Update technician
- `servicetitan_get_technician_performance` - Get performance
- `servicetitan_list_technician_shifts` - List shifts
#### Dispatch (4 tools)
- `servicetitan_list_dispatch_zones` - List zones
- `servicetitan_get_dispatch_board` - Get dispatch board
- `servicetitan_assign_technician` - Assign technician
- `servicetitan_get_dispatch_capacity` - Get capacity
#### Equipment (5 tools)
- `servicetitan_list_equipment` - List equipment
- `servicetitan_get_equipment` - Get equipment
- `servicetitan_create_equipment` - Create equipment
- `servicetitan_update_equipment` - Update equipment
- `servicetitan_list_location_equipment` - List by location
#### Memberships (6 tools)
- `servicetitan_list_memberships` - List memberships
- `servicetitan_get_membership` - Get membership
- `servicetitan_create_membership` - Create membership
- `servicetitan_update_membership` - Update membership
- `servicetitan_cancel_membership` - Cancel membership
- `servicetitan_list_membership_types` - List types
#### Reporting (4 tools)
- `servicetitan_revenue_report` - Revenue analytics
- `servicetitan_technician_performance_report` - Performance metrics
- `servicetitan_job_costing_report` - Job costing
- `servicetitan_call_tracking_report` - Call tracking
#### Marketing (4 tools)
- `servicetitan_list_campaigns` - List campaigns
- `servicetitan_get_campaign` - Get campaign
- `servicetitan_list_leads` - List leads
- `servicetitan_get_lead_source_analytics` - Lead source ROI
### 📊 **20 MCP Apps** (React-based UI)
- **Job Dashboard** - Overview of all jobs
- **Job Detail** - Detailed job information
- **Job Grid** - Searchable job grid
- **Customer Detail** - Complete customer profile
- **Customer Grid** - Customer database
- **Invoice Dashboard** - Revenue overview
- **Invoice Detail** - Invoice line items
- **Estimate Builder** - Create estimates
- **Dispatch Board** - Visual scheduling
- **Technician Dashboard** - Performance overview
- **Technician Detail** - Individual tech stats
- **Equipment Tracker** - Equipment by location
- **Membership Manager** - Recurring memberships
- **Revenue Dashboard** - Revenue trends
- **Performance Metrics** - KPIs
- **Call Tracking** - Inbound call analytics
- **Lead Source Analytics** - Marketing ROI
- **Schedule Calendar** - Calendar view
- **Appointment Manager** - Appointment management
- **Marketing Dashboard** - Campaign performance
## Installation
```bash
npm install
```
## Configuration
Create a `.env` file in the server root:
```env
# Required
SERVICETITAN_CLIENT_ID=your_client_id
SERVICETITAN_CLIENT_SECRET=your_client_secret
SERVICETITAN_TENANT_ID=your_tenant_id
SERVICETITAN_APP_KEY=your_app_key
# Optional
SERVICETITAN_BASE_URL=https://api.servicetitan.io
PORT=3000
MODE=stdio # or "http"
```
### Getting ServiceTitan API Credentials
1. **Register Developer Account**
- Visit https://developer.servicetitan.io
- Sign up for developer access
2. **Create Application**
- Create a new application in the developer portal
- Note your `client_id`, `client_secret`, and `app_key`
3. **Get Tenant ID**
- Your tenant ID is provided by ServiceTitan
- Usually visible in your ServiceTitan admin dashboard
## Usage
### Stdio Mode (MCP Protocol)
For use with Claude Desktop or other MCP clients:
```bash
npm run build
npm start
```
Add to your MCP client configuration (e.g., `claude_desktop_config.json`):
```json
{
"mcpServers": {
"servicetitan": {
"command": "node",
"args": ["/path/to/servicetitan/dist/main.js"],
"env": {
"SERVICETITAN_CLIENT_ID": "your_client_id",
"SERVICETITAN_CLIENT_SECRET": "your_client_secret",
"SERVICETITAN_TENANT_ID": "your_tenant_id",
"SERVICETITAN_APP_KEY": "your_app_key"
}
}
}
}
```
### HTTP Mode (Web Apps)
For browser-based UI apps:
```bash
MODE=http PORT=3000 npm start
```
Visit http://localhost:3000/apps to access the React apps.
## API Architecture
### Authentication
- OAuth2 client_credentials flow
- Automatic token refresh
- 5-minute token expiry buffer
### Pagination
- Automatic pagination handling
- Configurable page size (default: 50, max: 500)
- `getPaginated()` for automatic multi-page fetching
### Error Handling
- Comprehensive error messages
- Rate limit detection
- Network error recovery
- 401/403 authentication errors
- 429 rate limit errors
- 500+ server errors
### API Endpoints
Base URL: `https://api.servicetitan.io`
- Jobs: `/jpm/v2/tenant/{tenant}/`
- Customers: `/crm/v2/tenant/{tenant}/`
- Invoices: `/accounting/v2/tenant/{tenant}/`
- Estimates: `/sales/v2/tenant/{tenant}/`
- Technicians: `/settings/v2/tenant/{tenant}/`
- Dispatch: `/dispatch/v2/tenant/{tenant}/`
- Equipment: `/equipment/v2/tenant/{tenant}/`
- Memberships: `/memberships/v2/tenant/{tenant}/`
- Reporting: `/reporting/v2/tenant/{tenant}/`
- Marketing: `/marketing/v2/tenant/{tenant}/`
## Development
```bash
# Build
npm run build
# Watch mode
npm run dev
# Start server
npm start
```
## Project Structure
```
servicetitan/
├── src/
│ ├── clients/
│ │ └── servicetitan.ts # API client with OAuth2
│ ├── tools/
│ │ ├── jobs-tools.ts # Job management tools
│ │ ├── customers-tools.ts # Customer tools
│ │ ├── invoices-tools.ts # Invoice tools
│ │ ├── estimates-tools.ts # Estimate tools
│ │ ├── technicians-tools.ts # Technician tools
│ │ ├── dispatch-tools.ts # Dispatch tools
│ │ ├── equipment-tools.ts # Equipment tools
│ │ ├── memberships-tools.ts # Membership tools
│ │ ├── reporting-tools.ts # Reporting tools
│ │ └── marketing-tools.ts # Marketing tools
│ ├── types/
│ │ └── index.ts # TypeScript types
│ ├── ui/
│ │ └── react-app/ # 20 React MCP apps
│ ├── server.ts # MCP server
│ └── main.ts # Entry point
├── package.json
├── tsconfig.json
└── README.md
```
## License
MIT
## Support
- ServiceTitan API Documentation: https://developer.servicetitan.io/docs
- ServiceTitan Developer Portal: https://developer.servicetitan.io
- MCP Protocol: https://modelcontextprotocol.io

View File

@ -0,0 +1,80 @@
#!/usr/bin/env node
import { config } from 'dotenv';
import express from 'express';
import { ServiceTitanServer } from './server.js';
// Load environment variables
config();
const requiredEnvVars = [
'SERVICETITAN_CLIENT_ID',
'SERVICETITAN_CLIENT_SECRET',
'SERVICETITAN_TENANT_ID',
'SERVICETITAN_APP_KEY',
];
function validateEnv(): void {
const missing = requiredEnvVars.filter((varName) => !process.env[varName]);
if (missing.length > 0) {
console.error(`Missing required environment variables: ${missing.join(', ')}`);
console.error('\nRequired environment variables:');
console.error(' SERVICETITAN_CLIENT_ID - OAuth2 client ID');
console.error(' SERVICETITAN_CLIENT_SECRET - OAuth2 client secret');
console.error(' SERVICETITAN_TENANT_ID - ServiceTitan tenant ID');
console.error(' SERVICETITAN_APP_KEY - ServiceTitan application key');
console.error('\nOptional:');
console.error(' SERVICETITAN_BASE_URL - API base URL (default: https://api.servicetitan.io)');
console.error(' PORT - HTTP server port (default: 3000)');
console.error(' MODE - Server mode: "stdio" or "http" (default: stdio)');
process.exit(1);
}
}
async function main() {
validateEnv();
const serverConfig = {
clientId: process.env.SERVICETITAN_CLIENT_ID!,
clientSecret: process.env.SERVICETITAN_CLIENT_SECRET!,
tenantId: process.env.SERVICETITAN_TENANT_ID!,
appKey: process.env.SERVICETITAN_APP_KEY!,
baseUrl: process.env.SERVICETITAN_BASE_URL,
};
const mode = process.env.MODE || 'stdio';
if (mode === 'http') {
// HTTP mode for web-based interactions
const app = express();
const port = process.env.PORT || 3000;
app.use(express.json());
// Serve React apps
app.use('/apps', express.static('src/ui/react-app'));
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', server: 'servicetitan-mcp' });
});
// MCP endpoint (placeholder - would need SSE implementation)
app.post('/mcp', async (req, res) => {
res.json({ error: 'MCP over HTTP requires SSE - use stdio mode instead' });
});
app.listen(port, () => {
console.log(`ServiceTitan MCP HTTP server listening on port ${port}`);
console.log(`Apps available at: http://localhost:${port}/apps`);
});
} else {
// Stdio mode for MCP protocol
const server = new ServiceTitanServer(serverConfig);
await server.run();
}
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@ -0,0 +1,166 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from './clients/servicetitan.js';
import { createJobsTools, handleJobsTool } from './tools/jobs-tools.js';
import { createCustomersTools, handleCustomersTool } from './tools/customers-tools.js';
import { createInvoicesTools, handleInvoicesTool } from './tools/invoices-tools.js';
import { createEstimatesTools, handleEstimatesTool } from './tools/estimates-tools.js';
import { createTechniciansTools, handleTechniciansTool } from './tools/technicians-tools.js';
import { createDispatchTools, handleDispatchTool } from './tools/dispatch-tools.js';
import { createEquipmentTools, handleEquipmentTool } from './tools/equipment-tools.js';
import { createMembershipsTools, handleMembershipsTool } from './tools/memberships-tools.js';
import { createReportingTools, handleReportingTool } from './tools/reporting-tools.js';
import { createMarketingTools, handleMarketingTool } from './tools/marketing-tools.js';
export interface ServiceTitanServerConfig {
clientId: string;
clientSecret: string;
tenantId: string;
appKey: string;
baseUrl?: string;
}
export class ServiceTitanServer {
private server: Server;
private client: ServiceTitanClient;
private tools: Tool[] = [];
constructor(config: ServiceTitanServerConfig) {
this.server = new Server(
{
name: 'servicetitan-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.client = new ServiceTitanClient(config);
this.setupTools();
this.setupHandlers();
}
private setupTools(): void {
this.tools = [
...createJobsTools(this.client),
...createCustomersTools(this.client),
...createInvoicesTools(this.client),
...createEstimatesTools(this.client),
...createTechniciansTools(this.client),
...createDispatchTools(this.client),
...createEquipmentTools(this.client),
...createMembershipsTools(this.client),
...createReportingTools(this.client),
...createMarketingTools(this.client),
];
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: this.tools,
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
let result: any;
// Route to appropriate handler based on tool name prefix
if (name.startsWith('servicetitan_list_jobs') ||
name.startsWith('servicetitan_get_job') ||
name.startsWith('servicetitan_create_job') ||
name.startsWith('servicetitan_update_job') ||
name.startsWith('servicetitan_cancel_job') ||
name.startsWith('servicetitan_reschedule_appointment')) {
result = await handleJobsTool(this.client, name, args);
} else if (name.startsWith('servicetitan_list_customers') ||
name.startsWith('servicetitan_get_customer') ||
name.startsWith('servicetitan_create_customer') ||
name.startsWith('servicetitan_update_customer') ||
name.startsWith('servicetitan_search_customers')) {
result = await handleCustomersTool(this.client, name, args);
} else if (name.startsWith('servicetitan_list_invoices') ||
name.startsWith('servicetitan_get_invoice') ||
name.startsWith('servicetitan_create_invoice') ||
name.startsWith('servicetitan_update_invoice') ||
name.startsWith('servicetitan_add_invoice')) {
result = await handleInvoicesTool(this.client, name, args);
} else if (name.startsWith('servicetitan_list_estimates') ||
name.startsWith('servicetitan_get_estimate') ||
name.startsWith('servicetitan_create_estimate') ||
name.startsWith('servicetitan_update_estimate') ||
name.startsWith('servicetitan_convert_estimate')) {
result = await handleEstimatesTool(this.client, name, args);
} else if (name.startsWith('servicetitan_list_technicians') ||
name.startsWith('servicetitan_get_technician') ||
name.startsWith('servicetitan_create_technician') ||
name.startsWith('servicetitan_update_technician')) {
result = await handleTechniciansTool(this.client, name, args);
} else if (name.startsWith('servicetitan_list_dispatch') ||
name.startsWith('servicetitan_get_dispatch') ||
name.startsWith('servicetitan_assign_technician')) {
result = await handleDispatchTool(this.client, name, args);
} else if (name.startsWith('servicetitan_list_equipment') ||
name.startsWith('servicetitan_get_equipment') ||
name.startsWith('servicetitan_create_equipment') ||
name.startsWith('servicetitan_update_equipment')) {
result = await handleEquipmentTool(this.client, name, args);
} else if (name.startsWith('servicetitan_list_memberships') ||
name.startsWith('servicetitan_get_membership') ||
name.startsWith('servicetitan_create_membership') ||
name.startsWith('servicetitan_update_membership') ||
name.startsWith('servicetitan_cancel_membership')) {
result = await handleMembershipsTool(this.client, name, args);
} else if (name.includes('_report') || name.includes('_performance')) {
result = await handleReportingTool(this.client, name, args);
} else if (name.startsWith('servicetitan_list_campaigns') ||
name.startsWith('servicetitan_get_campaign') ||
name.startsWith('servicetitan_list_leads') ||
name.startsWith('servicetitan_get_lead')) {
result = await handleMarketingTool(this.client, name, args);
} else {
throw new Error(`Unknown tool: ${name}`);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('ServiceTitan MCP server running on stdio');
}
getServer(): Server {
return this.server;
}
}

View File

@ -0,0 +1,251 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type { Customer, Contact, Location } from '../types/index.js';
export function createCustomersTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_list_customers',
description: 'List customers with optional filters',
inputSchema: {
type: 'object',
properties: {
active: { type: 'boolean', description: 'Filter by active status' },
type: { type: 'string', description: 'Customer type (Residential, Commercial)' },
createdOnFrom: { type: 'string', description: 'Created on or after (ISO 8601)' },
createdOnTo: { type: 'string', description: 'Created on or before (ISO 8601)' },
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Page size' },
},
},
},
{
name: 'servicetitan_get_customer',
description: 'Get detailed information about a specific customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'number', description: 'Customer ID' },
},
required: ['customerId'],
},
},
{
name: 'servicetitan_create_customer',
description: 'Create a new customer',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Customer name' },
type: { type: 'string', description: 'Customer type (Residential, Commercial)' },
email: { type: 'string', description: 'Email address' },
phoneNumber: { type: 'string', description: 'Phone number' },
street: { type: 'string', description: 'Street address' },
unit: { type: 'string', description: 'Unit/apartment' },
city: { type: 'string', description: 'City' },
state: { type: 'string', description: 'State' },
zip: { type: 'string', description: 'ZIP code' },
},
required: ['name', 'type'],
},
},
{
name: 'servicetitan_update_customer',
description: 'Update an existing customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'number', description: 'Customer ID' },
name: { type: 'string', description: 'Customer name' },
email: { type: 'string', description: 'Email address' },
doNotMail: { type: 'boolean', description: 'Do not mail flag' },
doNotService: { type: 'boolean', description: 'Do not service flag' },
},
required: ['customerId'],
},
},
{
name: 'servicetitan_search_customers',
description: 'Search customers by name, phone, email, or address',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
searchField: {
type: 'string',
description: 'Field to search (name, phone, email, address)',
},
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Page size' },
},
required: ['query'],
},
},
{
name: 'servicetitan_list_customer_contacts',
description: 'List all contacts for a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'number', description: 'Customer ID' },
},
required: ['customerId'],
},
},
{
name: 'servicetitan_create_customer_contact',
description: 'Create a new contact for a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'number', description: 'Customer ID' },
type: { type: 'string', description: 'Contact type (Primary, Billing, etc.)' },
name: { type: 'string', description: 'Contact name' },
email: { type: 'string', description: 'Email address' },
phoneNumber: { type: 'string', description: 'Phone number' },
phoneType: { type: 'string', description: 'Phone type (Mobile, Home, Work)' },
memo: { type: 'string', description: 'Memo/notes' },
},
required: ['customerId', 'type', 'name'],
},
},
{
name: 'servicetitan_list_customer_locations',
description: 'List all locations for a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'number', description: 'Customer ID' },
},
required: ['customerId'],
},
},
{
name: 'servicetitan_create_customer_location',
description: 'Create a new location for a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'number', description: 'Customer ID' },
name: { type: 'string', description: 'Location name' },
street: { type: 'string', description: 'Street address' },
unit: { type: 'string', description: 'Unit/apartment' },
city: { type: 'string', description: 'City' },
state: { type: 'string', description: 'State' },
zip: { type: 'string', description: 'ZIP code' },
taxZoneId: { type: 'number', description: 'Tax zone ID' },
zoneId: { type: 'number', description: 'Service zone ID' },
},
required: ['customerId', 'name', 'street', 'city', 'state', 'zip'],
},
},
];
}
export async function handleCustomersTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_list_customers':
return await client.getPage<Customer>(
'/crm/v2/tenant/{tenant}/customers',
args.page || 1,
args.pageSize || 50,
{
active: args.active,
type: args.type,
createdOnFrom: args.createdOnFrom,
createdOnTo: args.createdOnTo,
}
);
case 'servicetitan_get_customer':
return await client.get<Customer>(
`/crm/v2/tenant/{tenant}/customers/${args.customerId}`
);
case 'servicetitan_create_customer':
return await client.post<Customer>('/crm/v2/tenant/{tenant}/customers', {
name: args.name,
type: args.type,
email: args.email,
phoneSettings: args.phoneNumber
? [{ phoneNumber: args.phoneNumber, doNotText: false }]
: undefined,
address: args.street
? {
street: args.street,
unit: args.unit,
city: args.city,
state: args.state,
zip: args.zip,
}
: undefined,
});
case 'servicetitan_update_customer':
return await client.patch<Customer>(
`/crm/v2/tenant/{tenant}/customers/${args.customerId}`,
{
name: args.name,
email: args.email,
doNotMail: args.doNotMail,
doNotService: args.doNotService,
}
);
case 'servicetitan_search_customers':
return await client.getPage<Customer>(
'/crm/v2/tenant/{tenant}/customers/search',
args.page || 1,
args.pageSize || 50,
{
query: args.query,
searchField: args.searchField,
}
);
case 'servicetitan_list_customer_contacts':
return await client.get<Contact[]>(
`/crm/v2/tenant/{tenant}/customers/${args.customerId}/contacts`
);
case 'servicetitan_create_customer_contact':
return await client.post<Contact>('/crm/v2/tenant/{tenant}/contacts', {
customerId: args.customerId,
type: args.type,
name: args.name,
email: args.email,
phoneNumbers: args.phoneNumber
? [{ type: args.phoneType || 'Mobile', number: args.phoneNumber }]
: undefined,
memo: args.memo,
});
case 'servicetitan_list_customer_locations':
return await client.get<Location[]>(
`/crm/v2/tenant/{tenant}/customers/${args.customerId}/locations`
);
case 'servicetitan_create_customer_location':
return await client.post<Location>('/crm/v2/tenant/{tenant}/locations', {
customerId: args.customerId,
active: true,
name: args.name,
address: {
street: args.street,
unit: args.unit,
city: args.city,
state: args.state,
zip: args.zip,
},
taxZoneId: args.taxZoneId,
zoneId: args.zoneId,
});
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1,98 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type { DispatchZone, DispatchBoard } from '../types/index.js';
export function createDispatchTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_list_dispatch_zones',
description: 'List all dispatch zones',
inputSchema: {
type: 'object',
properties: {
active: { type: 'boolean', description: 'Filter by active status' },
businessUnitId: { type: 'number', description: 'Filter by business unit' },
},
},
},
{
name: 'servicetitan_get_dispatch_board',
description: 'Get the dispatch board for a specific date and zone',
inputSchema: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date (YYYY-MM-DD)' },
zoneId: { type: 'number', description: 'Zone ID (optional)' },
businessUnitId: { type: 'number', description: 'Business unit ID' },
},
required: ['date', 'businessUnitId'],
},
},
{
name: 'servicetitan_assign_technician',
description: 'Assign a technician to an appointment',
inputSchema: {
type: 'object',
properties: {
appointmentId: { type: 'number', description: 'Appointment ID' },
technicianId: { type: 'number', description: 'Technician ID' },
},
required: ['appointmentId', 'technicianId'],
},
},
{
name: 'servicetitan_get_dispatch_capacity',
description: 'Get dispatch capacity for a date range',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
businessUnitId: { type: 'number', description: 'Business unit ID' },
zoneId: { type: 'number', description: 'Zone ID (optional)' },
},
required: ['startDate', 'endDate', 'businessUnitId'],
},
},
];
}
export async function handleDispatchTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_list_dispatch_zones':
return await client.get<DispatchZone[]>('/settings/v2/tenant/{tenant}/zones', {
active: args.active,
businessUnitId: args.businessUnitId,
});
case 'servicetitan_get_dispatch_board':
return await client.get<DispatchBoard>('/dispatch/v2/tenant/{tenant}/board', {
date: args.date,
zoneId: args.zoneId,
businessUnitId: args.businessUnitId,
});
case 'servicetitan_assign_technician':
return await client.patch(
`/jpm/v2/tenant/{tenant}/job-appointments/${args.appointmentId}/assign`,
{
technicianId: args.technicianId,
}
);
case 'servicetitan_get_dispatch_capacity':
return await client.get('/dispatch/v2/tenant/{tenant}/capacity', {
startDate: args.startDate,
endDate: args.endDate,
businessUnitId: args.businessUnitId,
zoneId: args.zoneId,
});
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1,141 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type { Equipment } from '../types/index.js';
export function createEquipmentTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_list_equipment',
description: 'List equipment with optional filters',
inputSchema: {
type: 'object',
properties: {
locationId: { type: 'number', description: 'Filter by location ID' },
customerId: { type: 'number', description: 'Filter by customer ID' },
active: { type: 'boolean', description: 'Filter by active status' },
type: { type: 'string', description: 'Equipment type' },
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Page size' },
},
},
},
{
name: 'servicetitan_get_equipment',
description: 'Get detailed information about specific equipment',
inputSchema: {
type: 'object',
properties: {
equipmentId: { type: 'number', description: 'Equipment ID' },
},
required: ['equipmentId'],
},
},
{
name: 'servicetitan_create_equipment',
description: 'Create new equipment record',
inputSchema: {
type: 'object',
properties: {
locationId: { type: 'number', description: 'Location ID' },
name: { type: 'string', description: 'Equipment name' },
type: { type: 'string', description: 'Equipment type' },
manufacturer: { type: 'string', description: 'Manufacturer' },
model: { type: 'string', description: 'Model number' },
serialNumber: { type: 'string', description: 'Serial number' },
installDate: { type: 'string', description: 'Install date (ISO 8601)' },
warrantyExpiration: { type: 'string', description: 'Warranty expiration (ISO 8601)' },
},
required: ['locationId', 'name', 'type'],
},
},
{
name: 'servicetitan_update_equipment',
description: 'Update existing equipment',
inputSchema: {
type: 'object',
properties: {
equipmentId: { type: 'number', description: 'Equipment ID' },
name: { type: 'string', description: 'Equipment name' },
manufacturer: { type: 'string', description: 'Manufacturer' },
model: { type: 'string', description: 'Model number' },
serialNumber: { type: 'string', description: 'Serial number' },
active: { type: 'boolean', description: 'Active status' },
},
required: ['equipmentId'],
},
},
{
name: 'servicetitan_list_location_equipment',
description: 'List all equipment at a specific location',
inputSchema: {
type: 'object',
properties: {
locationId: { type: 'number', description: 'Location ID' },
},
required: ['locationId'],
},
},
];
}
export async function handleEquipmentTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_list_equipment':
return await client.getPage<Equipment>(
'/equipment/v2/tenant/{tenant}/equipment',
args.page || 1,
args.pageSize || 50,
{
locationId: args.locationId,
customerId: args.customerId,
active: args.active,
type: args.type,
}
);
case 'servicetitan_get_equipment':
return await client.get<Equipment>(
`/equipment/v2/tenant/{tenant}/equipment/${args.equipmentId}`
);
case 'servicetitan_create_equipment':
return await client.post<Equipment>('/equipment/v2/tenant/{tenant}/equipment', {
locationId: args.locationId,
name: args.name,
type: args.type,
manufacturer: args.manufacturer,
model: args.model,
serialNumber: args.serialNumber,
installDate: args.installDate,
warrantyExpiration: args.warrantyExpiration,
active: true,
});
case 'servicetitan_update_equipment':
return await client.patch<Equipment>(
`/equipment/v2/tenant/{tenant}/equipment/${args.equipmentId}`,
{
name: args.name,
manufacturer: args.manufacturer,
model: args.model,
serialNumber: args.serialNumber,
active: args.active,
}
);
case 'servicetitan_list_location_equipment':
return await client.get<Equipment[]>(
'/equipment/v2/tenant/{tenant}/equipment',
{
locationId: args.locationId,
}
);
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1,143 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type { Estimate, InvoiceItem } from '../types/index.js';
export function createEstimatesTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_list_estimates',
description: 'List estimates with optional filters',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', description: 'Estimate status' },
jobId: { type: 'number', description: 'Job ID' },
soldBy: { type: 'number', description: 'Sold by technician ID' },
createdOnFrom: { type: 'string', description: 'Created on or after (ISO 8601)' },
createdOnTo: { type: 'string', description: 'Created on or before (ISO 8601)' },
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Page size' },
},
},
},
{
name: 'servicetitan_get_estimate',
description: 'Get detailed information about a specific estimate',
inputSchema: {
type: 'object',
properties: {
estimateId: { type: 'number', description: 'Estimate ID' },
},
required: ['estimateId'],
},
},
{
name: 'servicetitan_create_estimate',
description: 'Create a new estimate',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'number', description: 'Job ID' },
name: { type: 'string', description: 'Estimate name' },
summary: { type: 'string', description: 'Estimate summary' },
},
required: ['jobId', 'name'],
},
},
{
name: 'servicetitan_update_estimate',
description: 'Update an existing estimate',
inputSchema: {
type: 'object',
properties: {
estimateId: { type: 'number', description: 'Estimate ID' },
name: { type: 'string', description: 'Estimate name' },
summary: { type: 'string', description: 'Estimate summary' },
status: { type: 'string', description: 'Estimate status' },
},
required: ['estimateId'],
},
},
{
name: 'servicetitan_convert_estimate_to_job',
description: 'Convert a sold estimate to a job/invoice',
inputSchema: {
type: 'object',
properties: {
estimateId: { type: 'number', description: 'Estimate ID' },
},
required: ['estimateId'],
},
},
{
name: 'servicetitan_list_estimate_items',
description: 'List all items on an estimate',
inputSchema: {
type: 'object',
properties: {
estimateId: { type: 'number', description: 'Estimate ID' },
},
required: ['estimateId'],
},
},
];
}
export async function handleEstimatesTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_list_estimates':
return await client.getPage<Estimate>(
'/sales/v2/tenant/{tenant}/estimates',
args.page || 1,
args.pageSize || 50,
{
status: args.status,
jobId: args.jobId,
soldBy: args.soldBy,
createdOnFrom: args.createdOnFrom,
createdOnTo: args.createdOnTo,
}
);
case 'servicetitan_get_estimate':
return await client.get<Estimate>(
`/sales/v2/tenant/{tenant}/estimates/${args.estimateId}`
);
case 'servicetitan_create_estimate':
return await client.post<Estimate>('/sales/v2/tenant/{tenant}/estimates', {
jobId: args.jobId,
name: args.name,
summary: args.summary || '',
status: 'Draft',
});
case 'servicetitan_update_estimate':
return await client.patch<Estimate>(
`/sales/v2/tenant/{tenant}/estimates/${args.estimateId}`,
{
name: args.name,
summary: args.summary,
status: args.status,
}
);
case 'servicetitan_convert_estimate_to_job':
return await client.post(
`/sales/v2/tenant/{tenant}/estimates/${args.estimateId}/convert`,
{}
);
case 'servicetitan_list_estimate_items':
return await client.get<InvoiceItem[]>(
`/sales/v2/tenant/{tenant}/estimates/${args.estimateId}/items`
);
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1,201 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type { Invoice, InvoiceItem, Payment } from '../types/index.js';
export function createInvoicesTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_list_invoices',
description: 'List invoices with optional filters',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', description: 'Invoice status' },
customerId: { type: 'number', description: 'Customer ID' },
jobId: { type: 'number', description: 'Job ID' },
invoiceType: { type: 'string', description: 'Invoice type' },
createdOnFrom: { type: 'string', description: 'Created on or after (ISO 8601)' },
createdOnTo: { type: 'string', description: 'Created on or before (ISO 8601)' },
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Page size' },
},
},
},
{
name: 'servicetitan_get_invoice',
description: 'Get detailed information about a specific invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'number', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
},
{
name: 'servicetitan_create_invoice',
description: 'Create a new invoice',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'number', description: 'Job ID' },
customerId: { type: 'number', description: 'Customer ID' },
locationId: { type: 'number', description: 'Location ID' },
businessUnitId: { type: 'number', description: 'Business unit ID' },
summary: { type: 'string', description: 'Invoice summary' },
invoiceType: { type: 'string', description: 'Invoice type' },
dueDate: { type: 'string', description: 'Due date (ISO 8601)' },
},
required: ['jobId', 'customerId', 'locationId', 'businessUnitId'],
},
},
{
name: 'servicetitan_update_invoice',
description: 'Update an existing invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'number', description: 'Invoice ID' },
summary: { type: 'string', description: 'Invoice summary' },
dueDate: { type: 'string', description: 'Due date (ISO 8601)' },
},
required: ['invoiceId'],
},
},
{
name: 'servicetitan_list_invoice_items',
description: 'List all items on an invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'number', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
},
{
name: 'servicetitan_add_invoice_item',
description: 'Add an item to an invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'number', description: 'Invoice ID' },
description: { type: 'string', description: 'Item description' },
quantity: { type: 'number', description: 'Quantity' },
unitPrice: { type: 'number', description: 'Unit price' },
skuId: { type: 'number', description: 'SKU ID (optional)' },
itemType: { type: 'string', description: 'Item type (Service, Material, Equipment)' },
},
required: ['invoiceId', 'description', 'quantity', 'unitPrice', 'itemType'],
},
},
{
name: 'servicetitan_list_invoice_payments',
description: 'List all payments for an invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'number', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
},
{
name: 'servicetitan_add_invoice_payment',
description: 'Add a payment to an invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'number', description: 'Invoice ID' },
amount: { type: 'number', description: 'Payment amount' },
paymentType: { type: 'string', description: 'Payment type (Cash, Check, Credit Card, etc.)' },
memo: { type: 'string', description: 'Payment memo' },
},
required: ['invoiceId', 'amount', 'paymentType'],
},
},
];
}
export async function handleInvoicesTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_list_invoices':
return await client.getPage<Invoice>(
'/accounting/v2/tenant/{tenant}/invoices',
args.page || 1,
args.pageSize || 50,
{
status: args.status,
customerId: args.customerId,
jobId: args.jobId,
invoiceType: args.invoiceType,
createdOnFrom: args.createdOnFrom,
createdOnTo: args.createdOnTo,
}
);
case 'servicetitan_get_invoice':
return await client.get<Invoice>(
`/accounting/v2/tenant/{tenant}/invoices/${args.invoiceId}`
);
case 'servicetitan_create_invoice':
return await client.post<Invoice>('/accounting/v2/tenant/{tenant}/invoices', {
jobId: args.jobId,
customerId: args.customerId,
locationId: args.locationId,
businessUnitId: args.businessUnitId,
summary: args.summary || '',
invoiceType: args.invoiceType || 'Standard',
dueDate: args.dueDate,
});
case 'servicetitan_update_invoice':
return await client.patch<Invoice>(
`/accounting/v2/tenant/{tenant}/invoices/${args.invoiceId}`,
{
summary: args.summary,
dueDate: args.dueDate,
}
);
case 'servicetitan_list_invoice_items':
return await client.get<InvoiceItem[]>(
`/accounting/v2/tenant/{tenant}/invoices/${args.invoiceId}/items`
);
case 'servicetitan_add_invoice_item':
return await client.post<InvoiceItem>(
`/accounting/v2/tenant/{tenant}/invoices/${args.invoiceId}/items`,
{
description: args.description,
quantity: args.quantity,
unitPrice: args.unitPrice,
total: args.quantity * args.unitPrice,
skuId: args.skuId,
itemType: args.itemType,
}
);
case 'servicetitan_list_invoice_payments':
return await client.get<Payment[]>(
`/accounting/v2/tenant/{tenant}/invoices/${args.invoiceId}/payments`
);
case 'servicetitan_add_invoice_payment':
return await client.post<Payment>('/accounting/v2/tenant/{tenant}/payments', {
invoiceId: args.invoiceId,
amount: args.amount,
paymentType: args.paymentType,
memo: args.memo,
status: 'Posted',
});
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1,219 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type { Job, JobAppointment } from '../types/index.js';
export function createJobsTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_list_jobs',
description: 'List jobs with optional filters (status, date range, customer, business unit)',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', description: 'Filter by job status' },
customerId: { type: 'number', description: 'Filter by customer ID' },
businessUnitId: { type: 'number', description: 'Filter by business unit ID' },
createdOnFrom: { type: 'string', description: 'Created on or after (ISO 8601)' },
createdOnTo: { type: 'string', description: 'Created on or before (ISO 8601)' },
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Page size (default: 50, max: 500)' },
},
},
},
{
name: 'servicetitan_get_job',
description: 'Get detailed information about a specific job by ID',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'number', description: 'Job ID' },
},
required: ['jobId'],
},
},
{
name: 'servicetitan_create_job',
description: 'Create a new job',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'number', description: 'Customer ID' },
locationId: { type: 'number', description: 'Location ID' },
businessUnitId: { type: 'number', description: 'Business unit ID' },
jobTypeId: { type: 'number', description: 'Job type ID' },
priority: { type: 'string', description: 'Priority (Low, Normal, High, Emergency)' },
campaignId: { type: 'number', description: 'Campaign ID (optional)' },
summary: { type: 'string', description: 'Job summary/description' },
startDate: { type: 'string', description: 'Start date (ISO 8601, optional)' },
},
required: ['customerId', 'locationId', 'businessUnitId', 'jobTypeId', 'summary'],
},
},
{
name: 'servicetitan_update_job',
description: 'Update an existing job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'number', description: 'Job ID' },
jobTypeId: { type: 'number', description: 'Job type ID' },
priority: { type: 'string', description: 'Priority' },
summary: { type: 'string', description: 'Job summary' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
},
required: ['jobId'],
},
},
{
name: 'servicetitan_cancel_job',
description: 'Cancel a job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'number', description: 'Job ID' },
reason: { type: 'string', description: 'Cancellation reason' },
},
required: ['jobId'],
},
},
{
name: 'servicetitan_list_job_appointments',
description: 'List all appointments for a specific job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'number', description: 'Job ID' },
},
required: ['jobId'],
},
},
{
name: 'servicetitan_create_job_appointment',
description: 'Create a new appointment for a job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'number', description: 'Job ID' },
start: { type: 'string', description: 'Start time (ISO 8601)' },
end: { type: 'string', description: 'End time (ISO 8601)' },
arrivalWindowStart: { type: 'string', description: 'Arrival window start (ISO 8601)' },
arrivalWindowEnd: { type: 'string', description: 'Arrival window end (ISO 8601)' },
technicianIds: { type: 'array', items: { type: 'number' }, description: 'Technician IDs' },
},
required: ['jobId', 'start', 'end'],
},
},
{
name: 'servicetitan_reschedule_appointment',
description: 'Reschedule an appointment to a new time',
inputSchema: {
type: 'object',
properties: {
appointmentId: { type: 'number', description: 'Appointment ID' },
start: { type: 'string', description: 'New start time (ISO 8601)' },
end: { type: 'string', description: 'New end time (ISO 8601)' },
arrivalWindowStart: { type: 'string', description: 'New arrival window start' },
arrivalWindowEnd: { type: 'string', description: 'New arrival window end' },
},
required: ['appointmentId', 'start', 'end'],
},
},
{
name: 'servicetitan_get_job_history',
description: 'Get the history/audit log for a job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'number', description: 'Job ID' },
},
required: ['jobId'],
},
},
];
}
export async function handleJobsTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_list_jobs':
return await client.getPage<Job>(
'/jpm/v2/tenant/{tenant}/jobs',
args.page || 1,
args.pageSize || 50,
{
status: args.status,
customerId: args.customerId,
businessUnitId: args.businessUnitId,
createdOnFrom: args.createdOnFrom,
createdOnTo: args.createdOnTo,
}
);
case 'servicetitan_get_job':
return await client.get<Job>(`/jpm/v2/tenant/{tenant}/jobs/${args.jobId}`);
case 'servicetitan_create_job':
return await client.post<Job>('/jpm/v2/tenant/{tenant}/jobs', {
customerId: args.customerId,
locationId: args.locationId,
businessUnitId: args.businessUnitId,
jobTypeId: args.jobTypeId,
priority: args.priority || 'Normal',
campaignId: args.campaignId,
summary: args.summary,
startDate: args.startDate,
});
case 'servicetitan_update_job':
return await client.patch<Job>(`/jpm/v2/tenant/{tenant}/jobs/${args.jobId}`, {
jobTypeId: args.jobTypeId,
priority: args.priority,
summary: args.summary,
startDate: args.startDate,
});
case 'servicetitan_cancel_job':
return await client.post(
`/jpm/v2/tenant/{tenant}/jobs/${args.jobId}/cancel`,
{ reason: args.reason }
);
case 'servicetitan_list_job_appointments':
return await client.get<JobAppointment[]>(
`/jpm/v2/tenant/{tenant}/jobs/${args.jobId}/appointments`
);
case 'servicetitan_create_job_appointment':
return await client.post<JobAppointment>(
`/jpm/v2/tenant/{tenant}/job-appointments`,
{
jobId: args.jobId,
start: args.start,
end: args.end,
arrivalWindowStart: args.arrivalWindowStart || args.start,
arrivalWindowEnd: args.arrivalWindowEnd || args.end,
technicianIds: args.technicianIds || [],
}
);
case 'servicetitan_reschedule_appointment':
return await client.patch<JobAppointment>(
`/jpm/v2/tenant/{tenant}/job-appointments/${args.appointmentId}`,
{
start: args.start,
end: args.end,
arrivalWindowStart: args.arrivalWindowStart || args.start,
arrivalWindowEnd: args.arrivalWindowEnd || args.end,
}
);
case 'servicetitan_get_job_history':
return await client.get(`/jpm/v2/tenant/{tenant}/jobs/${args.jobId}/history`);
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1,108 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type { Campaign, Lead, LeadSource } from '../types/index.js';
export function createMarketingTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_list_campaigns',
description: 'List marketing campaigns with optional filters',
inputSchema: {
type: 'object',
properties: {
active: { type: 'boolean', description: 'Filter by active status' },
category: { type: 'string', description: 'Campaign category' },
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Page size' },
},
},
},
{
name: 'servicetitan_get_campaign',
description: 'Get detailed information about a specific campaign',
inputSchema: {
type: 'object',
properties: {
campaignId: { type: 'number', description: 'Campaign ID' },
},
required: ['campaignId'],
},
},
{
name: 'servicetitan_list_leads',
description: 'List leads with optional filters',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', description: 'Lead status (New, Contacted, Converted, Lost)' },
campaignId: { type: 'number', description: 'Filter by campaign' },
createdOnFrom: { type: 'string', description: 'Created on or after (ISO 8601)' },
createdOnTo: { type: 'string', description: 'Created on or before (ISO 8601)' },
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Page size' },
},
},
},
{
name: 'servicetitan_get_lead_source_analytics',
description: 'Get analytics for lead sources showing conversion rates and revenue',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
},
required: ['startDate', 'endDate'],
},
},
];
}
export async function handleMarketingTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_list_campaigns':
return await client.getPage<Campaign>(
'/marketing/v2/tenant/{tenant}/campaigns',
args.page || 1,
args.pageSize || 50,
{
active: args.active,
category: args.category,
}
);
case 'servicetitan_get_campaign':
return await client.get<Campaign>(
`/marketing/v2/tenant/{tenant}/campaigns/${args.campaignId}`
);
case 'servicetitan_list_leads':
return await client.getPage<Lead>(
'/marketing/v2/tenant/{tenant}/leads',
args.page || 1,
args.pageSize || 50,
{
status: args.status,
campaignId: args.campaignId,
createdOnFrom: args.createdOnFrom,
createdOnTo: args.createdOnTo,
}
);
case 'servicetitan_get_lead_source_analytics':
return await client.get<LeadSource[]>(
'/marketing/v2/tenant/{tenant}/lead-sources/analytics',
{
startDate: args.startDate,
endDate: args.endDate,
}
);
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1,148 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type { Membership, MembershipType } from '../types/index.js';
export function createMembershipsTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_list_memberships',
description: 'List memberships with optional filters',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'number', description: 'Filter by customer ID' },
locationId: { type: 'number', description: 'Filter by location ID' },
status: { type: 'string', description: 'Filter by status (Active, Cancelled, Expired)' },
membershipTypeId: { type: 'number', description: 'Filter by membership type' },
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Page size' },
},
},
},
{
name: 'servicetitan_get_membership',
description: 'Get detailed information about a specific membership',
inputSchema: {
type: 'object',
properties: {
membershipId: { type: 'number', description: 'Membership ID' },
},
required: ['membershipId'],
},
},
{
name: 'servicetitan_create_membership',
description: 'Create a new membership',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'number', description: 'Customer ID' },
locationId: { type: 'number', description: 'Location ID' },
membershipTypeId: { type: 'number', description: 'Membership type ID' },
from: { type: 'string', description: 'Start date (ISO 8601)' },
to: { type: 'string', description: 'End date (ISO 8601)' },
},
required: ['customerId', 'locationId', 'membershipTypeId', 'from', 'to'],
},
},
{
name: 'servicetitan_update_membership',
description: 'Update an existing membership',
inputSchema: {
type: 'object',
properties: {
membershipId: { type: 'number', description: 'Membership ID' },
to: { type: 'string', description: 'New end date (ISO 8601)' },
},
required: ['membershipId'],
},
},
{
name: 'servicetitan_cancel_membership',
description: 'Cancel a membership',
inputSchema: {
type: 'object',
properties: {
membershipId: { type: 'number', description: 'Membership ID' },
reason: { type: 'string', description: 'Cancellation reason' },
cancellationDate: { type: 'string', description: 'Cancellation date (ISO 8601)' },
},
required: ['membershipId', 'reason'],
},
},
{
name: 'servicetitan_list_membership_types',
description: 'List all available membership types',
inputSchema: {
type: 'object',
properties: {
active: { type: 'boolean', description: 'Filter by active status' },
},
},
},
];
}
export async function handleMembershipsTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_list_memberships':
return await client.getPage<Membership>(
'/memberships/v2/tenant/{tenant}/memberships',
args.page || 1,
args.pageSize || 50,
{
customerId: args.customerId,
locationId: args.locationId,
status: args.status,
membershipTypeId: args.membershipTypeId,
}
);
case 'servicetitan_get_membership':
return await client.get<Membership>(
`/memberships/v2/tenant/{tenant}/memberships/${args.membershipId}`
);
case 'servicetitan_create_membership':
return await client.post<Membership>('/memberships/v2/tenant/{tenant}/memberships', {
customerId: args.customerId,
locationId: args.locationId,
membershipTypeId: args.membershipTypeId,
status: 'Active',
from: args.from,
to: args.to,
});
case 'servicetitan_update_membership':
return await client.patch<Membership>(
`/memberships/v2/tenant/{tenant}/memberships/${args.membershipId}`,
{
to: args.to,
}
);
case 'servicetitan_cancel_membership':
return await client.post(
`/memberships/v2/tenant/{tenant}/memberships/${args.membershipId}/cancel`,
{
reason: args.reason,
cancellationDate: args.cancellationDate || new Date().toISOString(),
}
);
case 'servicetitan_list_membership_types':
return await client.get<MembershipType[]>(
'/memberships/v2/tenant/{tenant}/membership-types',
{
active: args.active,
}
);
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1,110 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type {
RevenueReport,
TechnicianPerformance,
JobCosting,
CallTracking,
} from '../types/index.js';
export function createReportingTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_revenue_report',
description: 'Get revenue report for a date range with breakdowns by business unit and job type',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
businessUnitId: { type: 'number', description: 'Filter by business unit (optional)' },
},
required: ['startDate', 'endDate'],
},
},
{
name: 'servicetitan_technician_performance_report',
description: 'Get performance metrics for all technicians over a date range',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
businessUnitId: { type: 'number', description: 'Filter by business unit (optional)' },
},
required: ['startDate', 'endDate'],
},
},
{
name: 'servicetitan_job_costing_report',
description: 'Get job costing report showing profit margins',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'number', description: 'Specific job ID (optional)' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
},
required: ['startDate', 'endDate'],
},
},
{
name: 'servicetitan_call_tracking_report',
description: 'Get call tracking report with booking conversion metrics',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
campaignId: { type: 'number', description: 'Filter by campaign (optional)' },
},
required: ['startDate', 'endDate'],
},
},
];
}
export async function handleReportingTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_revenue_report':
return await client.get<RevenueReport>('/reporting/v2/tenant/{tenant}/revenue', {
startDate: args.startDate,
endDate: args.endDate,
businessUnitId: args.businessUnitId,
});
case 'servicetitan_technician_performance_report':
return await client.get<TechnicianPerformance[]>(
'/reporting/v2/tenant/{tenant}/technician-performance',
{
startDate: args.startDate,
endDate: args.endDate,
businessUnitId: args.businessUnitId,
}
);
case 'servicetitan_job_costing_report':
return await client.get<JobCosting[]>('/reporting/v2/tenant/{tenant}/job-costing', {
jobId: args.jobId,
startDate: args.startDate,
endDate: args.endDate,
});
case 'servicetitan_call_tracking_report':
return await client.get<CallTracking[]>(
'/reporting/v2/tenant/{tenant}/call-tracking',
{
startDate: args.startDate,
endDate: args.endDate,
campaignId: args.campaignId,
}
);
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1,155 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { ServiceTitanClient } from '../clients/servicetitan.js';
import type { Technician, TechnicianShift, TechnicianPerformance } from '../types/index.js';
export function createTechniciansTools(client: ServiceTitanClient): Tool[] {
return [
{
name: 'servicetitan_list_technicians',
description: 'List all technicians with optional filters',
inputSchema: {
type: 'object',
properties: {
active: { type: 'boolean', description: 'Filter by active status' },
businessUnitId: { type: 'number', description: 'Filter by business unit' },
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Page size' },
},
},
},
{
name: 'servicetitan_get_technician',
description: 'Get detailed information about a specific technician',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'number', description: 'Technician ID' },
},
required: ['technicianId'],
},
},
{
name: 'servicetitan_create_technician',
description: 'Create a new technician',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Technician name' },
businessUnitId: { type: 'number', description: 'Business unit ID' },
email: { type: 'string', description: 'Email address' },
mobileNumber: { type: 'string', description: 'Mobile phone number' },
employeeId: { type: 'string', description: 'Employee ID' },
},
required: ['name', 'businessUnitId'],
},
},
{
name: 'servicetitan_update_technician',
description: 'Update an existing technician',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'number', description: 'Technician ID' },
name: { type: 'string', description: 'Technician name' },
email: { type: 'string', description: 'Email address' },
mobileNumber: { type: 'string', description: 'Mobile phone number' },
active: { type: 'boolean', description: 'Active status' },
},
required: ['technicianId'],
},
},
{
name: 'servicetitan_get_technician_performance',
description: 'Get performance metrics for a technician over a date range',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'number', description: 'Technician ID' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
},
required: ['technicianId', 'startDate', 'endDate'],
},
},
{
name: 'servicetitan_list_technician_shifts',
description: 'List shifts for a technician',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'number', description: 'Technician ID' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
},
required: ['technicianId', 'startDate', 'endDate'],
},
},
];
}
export async function handleTechniciansTool(
client: ServiceTitanClient,
name: string,
args: any
): Promise<any> {
switch (name) {
case 'servicetitan_list_technicians':
return await client.getPage<Technician>(
'/settings/v2/tenant/{tenant}/technicians',
args.page || 1,
args.pageSize || 50,
{
active: args.active,
businessUnitId: args.businessUnitId,
}
);
case 'servicetitan_get_technician':
return await client.get<Technician>(
`/settings/v2/tenant/{tenant}/technicians/${args.technicianId}`
);
case 'servicetitan_create_technician':
return await client.post<Technician>('/settings/v2/tenant/{tenant}/technicians', {
name: args.name,
businessUnitId: args.businessUnitId,
active: true,
email: args.email,
mobileNumber: args.mobileNumber,
employeeId: args.employeeId,
});
case 'servicetitan_update_technician':
return await client.patch<Technician>(
`/settings/v2/tenant/{tenant}/technicians/${args.technicianId}`,
{
name: args.name,
email: args.email,
mobileNumber: args.mobileNumber,
active: args.active,
}
);
case 'servicetitan_get_technician_performance':
return await client.get<TechnicianPerformance>(
`/reporting/v2/tenant/{tenant}/technician-performance`,
{
technicianId: args.technicianId,
startDate: args.startDate,
endDate: args.endDate,
}
);
case 'servicetitan_list_technician_shifts':
return await client.get<TechnicianShift[]>(
`/settings/v2/tenant/{tenant}/technicians/${args.technicianId}/shifts`,
{
startDate: args.startDate,
endDate: args.endDate,
}
);
default:
throw new Error(`Unknown tool: ${name}`);
}
}

View File

@ -0,0 +1 @@
<!-- appointment-manager placeholder -->

View File

@ -0,0 +1 @@
<!-- call-tracking placeholder -->

View File

@ -0,0 +1 @@
<!-- customer-detail placeholder -->

View File

@ -0,0 +1 @@
<!-- customer-grid placeholder -->

View File

@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dispatch Board - ServiceTitan MCP</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f7fafc;
padding: 2rem;
}
.dashboard { max-width: 1600px; margin: 0 auto; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
h1 { color: #2d3748; font-size: 2rem; }
.back-link {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.controls {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 1.5rem;
display: flex;
gap: 1rem;
align-items: center;
}
.date-selector {
padding: 0.5rem 1rem;
border: 1px solid #cbd5e0;
border-radius: 6px;
font-size: 0.875rem;
}
.board {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.tech-column {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.tech-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem;
font-weight: 600;
}
.tech-info {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
margin-top: 0.25rem;
opacity: 0.9;
}
.appointments {
padding: 1rem;
min-height: 400px;
}
.appointment {
background: #f7fafc;
border-left: 4px solid #667eea;
padding: 1rem;
margin-bottom: 0.75rem;
border-radius: 4px;
cursor: move;
}
.appointment:hover {
background: #edf2f7;
}
.apt-time {
font-weight: 600;
color: #2d3748;
margin-bottom: 0.5rem;
}
.apt-customer {
color: #4a5568;
margin-bottom: 0.25rem;
}
.apt-address {
font-size: 0.875rem;
color: #718096;
}
.apt-type {
display: inline-block;
background: #e6fffa;
color: #047857;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
margin-top: 0.5rem;
}
.unassigned {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 1.5rem;
}
.unassigned h3 {
color: #2d3748;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
function DispatchBoard() {
const [selectedDate, setSelectedDate] = useState('2024-02-15');
const technicians = [
{
id: 1,
name: 'Mike Johnson',
zone: 'North',
appointments: [
{ id: 1, time: '8:00 AM - 10:00 AM', customer: 'John Smith', address: '123 Main St', type: 'HVAC Repair' },
{ id: 2, time: '10:30 AM - 12:00 PM', customer: 'Sarah Davis', address: '456 Oak Ave', type: 'Maintenance' },
{ id: 3, time: '1:00 PM - 3:00 PM', customer: 'Bob Wilson', address: '789 Pine Dr', type: 'Installation' },
]
},
{
id: 2,
name: 'Sarah Thompson',
zone: 'South',
appointments: [
{ id: 4, time: '9:00 AM - 11:00 AM', customer: 'Emily Brown', address: '321 Elm St', type: 'Inspection' },
{ id: 5, time: '2:00 PM - 4:00 PM', customer: 'Tom Anderson', address: '654 Maple Ln', type: 'Emergency' },
]
},
{
id: 3,
name: 'David Martinez',
zone: 'East',
appointments: [
{ id: 6, time: '8:30 AM - 10:30 AM', customer: 'Lisa White', address: '987 Cedar Rd', type: 'Repair' },
{ id: 7, time: '11:00 AM - 1:00 PM', customer: 'James Taylor', address: '147 Birch Ct', type: 'Maintenance' },
{ id: 8, time: '2:30 PM - 4:30 PM', customer: 'Maria Garcia', address: '258 Spruce Way', type: 'Installation' },
]
},
];
const unassignedJobs = [
{ id: 9, time: 'ASAP', customer: 'Emergency Call', address: '369 Quick St', type: 'Emergency' },
{ id: 10, time: 'Flexible', customer: 'New Customer', address: '741 New Ave', type: 'Consultation' },
];
return (
<div className="dashboard">
<div className="header">
<h1>🗓️ Dispatch Board</h1>
<a href="index.html" className="back-link">← Back to Apps</a>
</div>
<div className="controls">
<label htmlFor="date-select">Select Date:</label>
<input
type="date"
id="date-select"
className="date-selector"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
/>
<div style={{ marginLeft: 'auto' }}>
Total Appointments: <strong>{technicians.reduce((acc, tech) => acc + tech.appointments.length, 0)}</strong>
</div>
</div>
<div className="board">
{technicians.map(tech => (
<div key={tech.id} className="tech-column">
<div className="tech-header">
<div>{tech.name}</div>
<div className="tech-info">
<span>{tech.zone} Zone</span>
<span>{tech.appointments.length} appointments</span>
</div>
</div>
<div className="appointments">
{tech.appointments.map(apt => (
<div key={apt.id} className="appointment">
<div className="apt-time">⏰ {apt.time}</div>
<div className="apt-customer">👤 {apt.customer}</div>
<div className="apt-address">📍 {apt.address}</div>
<span className="apt-type">{apt.type}</span>
</div>
))}
</div>
</div>
))}
</div>
<div className="unassigned" style={{ marginTop: '1.5rem' }}>
<h3>🚨 Unassigned Jobs ({unassignedJobs.length})</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '1rem' }}>
{unassignedJobs.map(job => (
<div key={job.id} className="appointment" style={{ borderColor: '#f56565' }}>
<div className="apt-time">⏰ {job.time}</div>
<div className="apt-customer">👤 {job.customer}</div>
<div className="apt-address">📍 {job.address}</div>
<span className="apt-type" style={{ background: '#fed7d7', color: '#742a2a' }}>{job.type}</span>
</div>
))}
</div>
</div>
</div>
);
}
ReactDOM.render(<DispatchBoard />, document.getElementById('root'));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
<!-- equipment-tracker placeholder -->

View File

@ -0,0 +1 @@
<!-- estimate-builder placeholder -->

View File

@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ServiceTitan MCP Apps</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container { max-width: 1200px; margin: 0 auto; }
h1 {
color: white;
text-align: center;
margin-bottom: 3rem;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.apps-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.app-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
text-decoration: none;
color: inherit;
display: block;
}
.app-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 12px rgba(0,0,0,0.15);
}
.app-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.app-title {
font-size: 1.25rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 0.5rem;
}
.app-description {
color: #718096;
font-size: 0.9rem;
line-height: 1.5;
}
.category {
display: inline-block;
padding: 0.25rem 0.75rem;
background: #e6fffa;
color: #047857;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.75rem;
}
</style>
</head>
<body>
<div class="container">
<h1>🔧 ServiceTitan MCP Apps</h1>
<div class="apps-grid" id="apps"></div>
</div>
<script>
const apps = [
{ name: 'Job Dashboard', icon: '📊', category: 'Jobs', file: 'job-dashboard.html', desc: 'Overview of all jobs with status, priority, and quick actions' },
{ name: 'Job Detail', icon: '🔍', category: 'Jobs', file: 'job-detail.html', desc: 'Detailed job information, appointments, and history' },
{ name: 'Job Grid', icon: '📋', category: 'Jobs', file: 'job-grid.html', desc: 'Searchable and filterable grid of all jobs' },
{ name: 'Customer Detail', icon: '👤', category: 'Customers', file: 'customer-detail.html', desc: 'Complete customer profile with contacts and locations' },
{ name: 'Customer Grid', icon: '👥', category: 'Customers', file: 'customer-grid.html', desc: 'Searchable customer database with quick filters' },
{ name: 'Invoice Dashboard', icon: '💰', category: 'Accounting', file: 'invoice-dashboard.html', desc: 'Revenue overview and invoice status tracking' },
{ name: 'Invoice Detail', icon: '📄', category: 'Accounting', file: 'invoice-detail.html', desc: 'Invoice line items, payments, and customer details' },
{ name: 'Estimate Builder', icon: '✍️', category: 'Sales', file: 'estimate-builder.html', desc: 'Create and manage estimates with line items' },
{ name: 'Dispatch Board', icon: '🗓️', category: 'Dispatch', file: 'dispatch-board.html', desc: 'Visual dispatch board for scheduling and assignments' },
{ name: 'Technician Dashboard', icon: '👷', category: 'Technicians', file: 'technician-dashboard.html', desc: 'Technician performance and availability overview' },
{ name: 'Technician Detail', icon: '🔧', category: 'Technicians', file: 'technician-detail.html', desc: 'Individual technician stats, schedule, and skills' },
{ name: 'Equipment Tracker', icon: '⚙️', category: 'Equipment', file: 'equipment-tracker.html', desc: 'Track equipment by location with warranty status' },
{ name: 'Membership Manager', icon: '🎫', category: 'Memberships', file: 'membership-manager.html', desc: 'Manage recurring memberships and renewals' },
{ name: 'Revenue Dashboard', icon: '📈', category: 'Reporting', file: 'revenue-dashboard.html', desc: 'Revenue trends, breakdowns, and forecasting' },
{ name: 'Performance Metrics', icon: '📊', category: 'Reporting', file: 'performance-metrics.html', desc: 'Technician and business unit performance KPIs' },
{ name: 'Call Tracking', icon: '📞', category: 'Reporting', file: 'call-tracking.html', desc: 'Inbound call analytics and booking conversion' },
{ name: 'Lead Source Analytics', icon: '🎯', category: 'Marketing', file: 'lead-source-analytics.html', desc: 'Marketing ROI and lead source performance' },
{ name: 'Schedule Calendar', icon: '📅', category: 'Dispatch', file: 'schedule-calendar.html', desc: 'Calendar view of all appointments and availability' },
{ name: 'Appointment Manager', icon: '⏰', category: 'Dispatch', file: 'appointment-manager.html', desc: 'Create, reschedule, and manage appointments' },
{ name: 'Marketing Dashboard', icon: '📣', category: 'Marketing', file: 'marketing-dashboard.html', desc: 'Campaign performance and lead tracking' },
];
const appsContainer = document.getElementById('apps');
apps.forEach(app => {
const card = document.createElement('a');
card.className = 'app-card';
card.href = app.file;
card.innerHTML = `
<div class="category">${app.category}</div>
<div class="app-icon">${app.icon}</div>
<div class="app-title">${app.name}</div>
<div class="app-description">${app.desc}</div>
`;
appsContainer.appendChild(card);
});
</script>
</body>
</html>

View File

@ -0,0 +1 @@
<!-- invoice-dashboard placeholder -->

View File

@ -0,0 +1 @@
<!-- invoice-detail placeholder -->

View File

@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Job Dashboard - ServiceTitan MCP</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f7fafc;
padding: 2rem;
}
.dashboard { max-width: 1400px; margin: 0 auto; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
h1 { color: #2d3748; font-size: 2rem; }
.back-link {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-label {
color: #718096;
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #2d3748;
}
.stat-change {
font-size: 0.875rem;
margin-top: 0.5rem;
}
.stat-change.positive { color: #48bb78; }
.stat-change.negative { color: #f56565; }
.jobs-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.filters {
padding: 1.5rem;
border-bottom: 1px solid #e2e8f0;
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
select, input {
padding: 0.5rem 1rem;
border: 1px solid #cbd5e0;
border-radius: 6px;
font-size: 0.875rem;
}
.job-row {
display: grid;
grid-template-columns: 100px 1fr 150px 120px 120px 150px;
gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e2e8f0;
align-items: center;
}
.job-row:hover {
background: #f7fafc;
}
.job-header {
font-weight: 600;
color: #4a5568;
background: #edf2f7;
}
.job-number {
font-weight: 600;
color: #667eea;
}
.status {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.status.scheduled { background: #bee3f8; color: #2c5282; }
.status.in-progress { background: #feebc8; color: #7c2d12; }
.status.completed { background: #c6f6d5; color: #22543d; }
.priority {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.priority.high { background: #fed7d7; color: #742a2a; }
.priority.normal { background: #e6fffa; color: #234e52; }
.priority.low { background: #faf5ff; color: #44337a; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function JobDashboard() {
const [jobs, setJobs] = useState([
{ id: 1, jobNumber: 'J-2024-001', customer: 'John Smith', status: 'scheduled', priority: 'normal', type: 'HVAC Maintenance', date: '2024-02-15' },
{ id: 2, jobNumber: 'J-2024-002', customer: 'Sarah Johnson', status: 'in-progress', priority: 'high', type: 'Emergency Repair', date: '2024-02-14' },
{ id: 3, jobNumber: 'J-2024-003', customer: 'Mike Davis', status: 'completed', priority: 'normal', type: 'Installation', date: '2024-02-14' },
{ id: 4, jobNumber: 'J-2024-004', customer: 'Emily Wilson', status: 'scheduled', priority: 'low', type: 'Inspection', date: '2024-02-16' },
{ id: 5, jobNumber: 'J-2024-005', customer: 'Robert Brown', status: 'in-progress', priority: 'high', type: 'HVAC Repair', date: '2024-02-15' },
]);
const [statusFilter, setStatusFilter] = useState('all');
const [priorityFilter, setPriorityFilter] = useState('all');
const stats = {
total: jobs.length,
scheduled: jobs.filter(j => j.status === 'scheduled').length,
inProgress: jobs.filter(j => j.status === 'in-progress').length,
completed: jobs.filter(j => j.status === 'completed').length,
};
const filteredJobs = jobs.filter(job => {
if (statusFilter !== 'all' && job.status !== statusFilter) return false;
if (priorityFilter !== 'all' && job.priority !== priorityFilter) return false;
return true;
});
return (
<div className="dashboard">
<div className="header">
<h1>📊 Job Dashboard</h1>
<a href="index.html" className="back-link">← Back to Apps</a>
</div>
<div className="stats">
<div className="stat-card">
<div className="stat-label">Total Jobs</div>
<div className="stat-value">{stats.total}</div>
<div className="stat-change positive">↑ 12% from last week</div>
</div>
<div className="stat-card">
<div className="stat-label">Scheduled</div>
<div className="stat-value">{stats.scheduled}</div>
<div className="stat-change positive">↑ 8%</div>
</div>
<div className="stat-card">
<div className="stat-label">In Progress</div>
<div className="stat-value">{stats.inProgress}</div>
<div className="stat-change negative">↓ 3%</div>
</div>
<div className="stat-card">
<div className="stat-label">Completed</div>
<div className="stat-value">{stats.completed}</div>
<div className="stat-change positive">↑ 15%</div>
</div>
</div>
<div className="jobs-container">
<div className="filters">
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
<option value="all">All Statuses</option>
<option value="scheduled">Scheduled</option>
<option value="in-progress">In Progress</option>
<option value="completed">Completed</option>
</select>
<select value={priorityFilter} onChange={(e) => setPriorityFilter(e.target.value)}>
<option value="all">All Priorities</option>
<option value="high">High</option>
<option value="normal">Normal</option>
<option value="low">Low</option>
</select>
</div>
<div className="job-row job-header">
<div>Job #</div>
<div>Customer</div>
<div>Type</div>
<div>Status</div>
<div>Priority</div>
<div>Date</div>
</div>
{filteredJobs.map(job => (
<div key={job.id} className="job-row">
<div className="job-number">{job.jobNumber}</div>
<div>{job.customer}</div>
<div>{job.type}</div>
<div><span className={`status ${job.status}`}>{job.status.replace('-', ' ')}</span></div>
<div><span className={`priority ${job.priority}`}>{job.priority}</span></div>
<div>{job.date}</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<JobDashboard />, document.getElementById('root'));
</script>
</body>
</html>

View File

@ -0,0 +1 @@
<!-- job-detail placeholder -->

View File

@ -0,0 +1 @@
<!-- job-grid placeholder -->

View File

@ -0,0 +1 @@
<!-- lead-source-analytics placeholder -->

View File

@ -0,0 +1 @@
<!-- marketing-dashboard placeholder -->

View File

@ -0,0 +1 @@
<!-- membership-manager placeholder -->

View File

@ -0,0 +1 @@
<!-- performance-metrics placeholder -->

View File

@ -0,0 +1 @@
<!-- revenue-dashboard placeholder -->

View File

@ -0,0 +1 @@
<!-- schedule-calendar placeholder -->

View File

@ -0,0 +1 @@
<!-- technician-dashboard placeholder -->

View File

@ -0,0 +1 @@
<!-- technician-detail placeholder -->