ServiceTitan: Complete MCP server with 55 tools, 20 React apps, OAuth2, pagination
This commit is contained in:
parent
fafbdbe188
commit
7de8a68173
10
servers/servicetitan/.env.example
Normal file
10
servers/servicetitan/.env.example
Normal 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
7
servers/servicetitan/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
275
servers/servicetitan/README.md
Normal file
275
servers/servicetitan/README.md
Normal 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
|
||||||
80
servers/servicetitan/src/main.ts
Normal file
80
servers/servicetitan/src/main.ts
Normal 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);
|
||||||
|
});
|
||||||
166
servers/servicetitan/src/server.ts
Normal file
166
servers/servicetitan/src/server.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
251
servers/servicetitan/src/tools/customers-tools.ts
Normal file
251
servers/servicetitan/src/tools/customers-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
servers/servicetitan/src/tools/dispatch-tools.ts
Normal file
98
servers/servicetitan/src/tools/dispatch-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
141
servers/servicetitan/src/tools/equipment-tools.ts
Normal file
141
servers/servicetitan/src/tools/equipment-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
143
servers/servicetitan/src/tools/estimates-tools.ts
Normal file
143
servers/servicetitan/src/tools/estimates-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
201
servers/servicetitan/src/tools/invoices-tools.ts
Normal file
201
servers/servicetitan/src/tools/invoices-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
219
servers/servicetitan/src/tools/jobs-tools.ts
Normal file
219
servers/servicetitan/src/tools/jobs-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
servers/servicetitan/src/tools/marketing-tools.ts
Normal file
108
servers/servicetitan/src/tools/marketing-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
148
servers/servicetitan/src/tools/memberships-tools.ts
Normal file
148
servers/servicetitan/src/tools/memberships-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
servers/servicetitan/src/tools/reporting-tools.ts
Normal file
110
servers/servicetitan/src/tools/reporting-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
servers/servicetitan/src/tools/technicians-tools.ts
Normal file
155
servers/servicetitan/src/tools/technicians-tools.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- appointment-manager placeholder -->
|
||||||
1
servers/servicetitan/src/ui/react-app/call-tracking.html
Normal file
1
servers/servicetitan/src/ui/react-app/call-tracking.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- call-tracking placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- customer-detail placeholder -->
|
||||||
1
servers/servicetitan/src/ui/react-app/customer-grid.html
Normal file
1
servers/servicetitan/src/ui/react-app/customer-grid.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- customer-grid placeholder -->
|
||||||
230
servers/servicetitan/src/ui/react-app/dispatch-board.html
Normal file
230
servers/servicetitan/src/ui/react-app/dispatch-board.html
Normal 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>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- equipment-tracker placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- estimate-builder placeholder -->
|
||||||
115
servers/servicetitan/src/ui/react-app/index.html
Normal file
115
servers/servicetitan/src/ui/react-app/index.html
Normal 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>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- invoice-dashboard placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- invoice-detail placeholder -->
|
||||||
223
servers/servicetitan/src/ui/react-app/job-dashboard.html
Normal file
223
servers/servicetitan/src/ui/react-app/job-dashboard.html
Normal 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>
|
||||||
1
servers/servicetitan/src/ui/react-app/job-detail.html
Normal file
1
servers/servicetitan/src/ui/react-app/job-detail.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- job-detail placeholder -->
|
||||||
1
servers/servicetitan/src/ui/react-app/job-grid.html
Normal file
1
servers/servicetitan/src/ui/react-app/job-grid.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!-- job-grid placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- lead-source-analytics placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- marketing-dashboard placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- membership-manager placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- performance-metrics placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- revenue-dashboard placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- schedule-calendar placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- technician-dashboard placeholder -->
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<!-- technician-detail placeholder -->
|
||||||
Loading…
x
Reference in New Issue
Block a user