fieldedge: Complete MCP server with 87 tools and 16 React apps

- Multi-file architecture with API client, comprehensive types, 13 tool domains
- 87 total tools covering customers, jobs, invoices, estimates, equipment, technicians, scheduling, inventory, payments, reporting, locations, service agreements, tasks
- 16 dark-themed React MCP apps (Dashboard, Customers, Jobs, Scheduling, Invoices, Estimates, Technicians, Equipment, Inventory, Payments, Service Agreements, Reports, Tasks, Calendar, Map View, Price Book)
- Full TypeScript support with zero compilation errors
- Comprehensive README with API coverage details
- Bearer token authentication with rate limiting and error handling
This commit is contained in:
Jake Shore 2026-02-12 18:18:51 -05:00
parent ec4a7475d9
commit 601224bf70
616 changed files with 38114 additions and 17713 deletions

View File

@ -1,20 +1,31 @@
{ {
"name": "mcp-server-close", "name": "@mcpengine/close-server",
"version": "1.0.0", "version": "1.0.0",
"description": "Complete Close CRM MCP server with 60+ tools and 22 apps",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/main.js",
"bin": {
"close-mcp": "./dist/main.js"
},
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"start": "node dist/index.js", "dev": "tsc --watch",
"dev": "tsx src/index.ts" "start": "node dist/main.js",
"prepare": "npm run build"
}, },
"keywords": [
"mcp",
"close",
"crm",
"sales"
],
"author": "MCPEngine",
"license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0", "@modelcontextprotocol/sdk": "^1.0.4"
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.10.0", "@types/node": "^22.0.0",
"tsx": "^4.7.0", "typescript": "^5.7.2"
"typescript": "^5.3.0"
} }
} }

View File

@ -0,0 +1,68 @@
// Activity Feed MCP App
export function generateActivityFeed(activities: any[]) {
const getActivityIcon = (type: string) => {
const icons: Record<string, string> = {
note: '📝',
call: '📞',
email: '📧',
sms: '💬',
meeting: '📅',
};
return icons[type] || '📋';
};
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Activity Feed</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 900px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.feed { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.activity { display: flex; gap: 16px; padding: 20px; border-bottom: 1px solid #e5e7eb; }
.activity:last-child { border-bottom: none; }
.activity:hover { background: #f9fafb; }
.activity-icon { font-size: 32px; flex-shrink: 0; }
.activity-content { flex: 1; }
.activity-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px; }
.activity-type { font-weight: 600; font-size: 15px; color: #1a1a1a; }
.activity-time { font-size: 13px; color: #6b7280; }
.activity-body { font-size: 14px; color: #374151; line-height: 1.5; }
.activity-meta { margin-top: 8px; font-size: 12px; color: #6b7280; }
</style>
</head>
<body>
<div class="container">
<h1>📊 Activity Feed (${activities.length})</h1>
<div class="feed">
${activities.length > 0 ? activities.map((activity: any) => `
<div class="activity">
<div class="activity-icon">${getActivityIcon(activity._type)}</div>
<div class="activity-content">
<div class="activity-header">
<div class="activity-type">${activity._type ? activity._type.charAt(0).toUpperCase() + activity._type.slice(1) : 'Activity'}</div>
<div class="activity-time">${activity.date_created ? new Date(activity.date_created).toLocaleString() : ''}</div>
</div>
<div class="activity-body">
${activity.note || activity.text || activity.subject || 'No content'}
</div>
<div class="activity-meta">
${activity.user_name ? `👤 ${activity.user_name}` : ''}
${activity.lead_id ? ` • 🏢 Lead: ${activity.lead_id}` : ''}
${activity.contact_id ? ` • 👤 Contact: ${activity.contact_id}` : ''}
</div>
</div>
</div>
`).join('') : '<div style="padding: 40px; text-align: center; color: #6b7280;">No activities found</div>'}
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,65 @@
// Activity Timeline MCP App
export function generateActivityTimeline(activities: any[]) {
const sortedActivities = [...activities].sort((a, b) => {
const dateA = new Date(a.date_created || 0).getTime();
const dateB = new Date(b.date_created || 0).getTime();
return dateB - dateA;
});
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Activity Timeline</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1000px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.timeline { position: relative; padding-left: 40px; }
.timeline::before { content: ''; position: absolute; left: 12px; top: 0; bottom: 0; width: 2px; background: #e5e7eb; }
.timeline-item { position: relative; margin-bottom: 32px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.timeline-dot { position: absolute; left: -34px; top: 24px; width: 16px; height: 16px; border-radius: 50%; background: #3b82f6; border: 3px solid white; box-shadow: 0 0 0 2px #3b82f6; }
.timeline-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px; }
.timeline-type { font-weight: 600; font-size: 16px; color: #1a1a1a; }
.timeline-date { font-size: 13px; color: #6b7280; }
.timeline-content { font-size: 14px; color: #374151; line-height: 1.6; }
.timeline-meta { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #6b7280; }
</style>
</head>
<body>
<div class="container">
<h1> Activity Timeline</h1>
<div class="timeline">
${sortedActivities.map((activity: any) => `
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-header">
<div class="timeline-type">
${activity._type === 'note' ? '📝 Note' :
activity._type === 'call' ? '📞 Call' :
activity._type === 'email' ? '📧 Email' :
activity._type === 'sms' ? '💬 SMS' :
activity._type === 'meeting' ? '📅 Meeting' : '📋 Activity'}
</div>
<div class="timeline-date">${activity.date_created ? new Date(activity.date_created).toLocaleString() : 'Unknown date'}</div>
</div>
<div class="timeline-content">
${activity.note || activity.text || activity.subject || activity.title || 'No content available'}
</div>
<div class="timeline-meta">
${activity.user_name ? `Created by ${activity.user_name}` : ''}
${activity.lead_id ? ` • Lead: ${activity.lead_id}` : ''}
${activity.duration ? ` • Duration: ${Math.floor(activity.duration / 60)} min` : ''}
</div>
</div>
`).join('')}
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,75 @@
// Bulk Actions MCP App
export function generateBulkActions(data: any) {
const leads = data.leads || [];
const selectedCount = data.selectedCount || leads.length;
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bulk Actions</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.toolbar { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.selection-count { font-weight: 600; color: #3b82f6; padding: 8px 16px; background: #dbeafe; border-radius: 6px; }
.btn { padding: 10px 20px; border-radius: 6px; border: none; font-weight: 500; cursor: pointer; transition: all 0.2s; font-size: 14px; }
.btn:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
.btn-primary { background: #3b82f6; color: white; }
.btn-danger { background: #dc2626; color: white; }
.btn-secondary { background: #6b7280; color: white; }
.table-container { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; }
th { background: #f9fafb; padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; }
td { padding: 14px 16px; border-top: 1px solid #e5e7eb; font-size: 14px; }
tr:hover { background: #f9fafb; }
.checkbox { width: 18px; height: 18px; cursor: pointer; }
</style>
</head>
<body>
<div class="container">
<h1> Bulk Actions</h1>
<div class="toolbar">
<div class="selection-count">${selectedCount} selected</div>
<button class="btn btn-primary">📝 Edit Fields</button>
<button class="btn btn-secondary">🏷 Change Status</button>
<button class="btn btn-secondary">📧 Send Email</button>
<button class="btn btn-secondary">📞 Log Call</button>
<button class="btn btn-danger">🗑 Delete</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th style="width: 40px;"><input type="checkbox" class="checkbox" checked></th>
<th>Lead Name</th>
<th>Status</th>
<th>Contacts</th>
<th>Created</th>
</tr>
</thead>
<tbody>
${leads.slice(0, 50).map(lead => `
<tr>
<td><input type="checkbox" class="checkbox" checked></td>
<td><strong>${lead.name || lead.display_name}</strong></td>
<td>${lead.status_label || 'No Status'}</td>
<td>${lead.contacts?.length || 0}</td>
<td>${lead.date_created ? new Date(lead.date_created).toLocaleDateString() : 'N/A'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,89 @@
// Call Log MCP App
export function generateCallLog(calls: any[]) {
const totalCalls = calls.length;
const totalDuration = calls.reduce((sum: number, call: any) => sum + (call.duration || 0), 0);
const avgDuration = totalCalls > 0 ? totalDuration / totalCalls : 0;
const inbound = calls.filter(c => c.direction === 'inbound').length;
const outbound = calls.filter(c => c.direction === 'outbound').length;
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Call Log</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-label { color: #6b7280; font-size: 13px; text-transform: uppercase; margin-bottom: 8px; }
.stat-value { font-size: 32px; font-weight: 600; color: #1a1a1a; }
.table-container { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; }
th { background: #f9fafb; padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; }
td { padding: 14px 16px; border-top: 1px solid #e5e7eb; font-size: 14px; }
tr:hover { background: #f9fafb; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.badge-inbound { background: #d1fae5; color: #065f46; }
.badge-outbound { background: #dbeafe; color: #1e40af; }
</style>
</head>
<body>
<div class="container">
<h1>📞 Call Log</h1>
<div class="stats">
<div class="stat-card">
<div class="stat-label">Total Calls</div>
<div class="stat-value">${totalCalls}</div>
</div>
<div class="stat-card">
<div class="stat-label">Inbound</div>
<div class="stat-value" style="color: #059669;">${inbound}</div>
</div>
<div class="stat-card">
<div class="stat-label">Outbound</div>
<div class="stat-value" style="color: #3b82f6;">${outbound}</div>
</div>
<div class="stat-card">
<div class="stat-label">Avg Duration</div>
<div class="stat-value">${Math.round(avgDuration / 60)}<span style="font-size: 16px; color: #6b7280;">min</span></div>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Date</th>
<th>Direction</th>
<th>Phone</th>
<th>Duration</th>
<th>Disposition</th>
<th>User</th>
</tr>
</thead>
<tbody>
${calls.length > 0 ? calls.map((call: any) => `
<tr>
<td>${call.date_created ? new Date(call.date_created).toLocaleString() : 'N/A'}</td>
<td><span class="badge badge-${call.direction}">${call.direction || 'Unknown'}</span></td>
<td>${call.phone || 'N/A'}</td>
<td>${call.duration ? Math.floor(call.duration / 60) + ' min ' + (call.duration % 60) + ' sec' : 'N/A'}</td>
<td>${call.disposition || 'N/A'}</td>
<td>${call.user_name || 'N/A'}</td>
</tr>
`).join('') : '<tr><td colspan="6" style="text-align: center; padding: 40px; color: #6b7280;">No calls found</td></tr>'}
</tbody>
</table>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,106 @@
// Contact Detail MCP App
export function generateContactDetail(contact: any) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${contact.name} - Contact Detail</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 900px; margin: 0 auto; }
.header { background: white; padding: 32px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center; }
.avatar { width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; font-size: 36px; color: white; margin: 0 auto 16px; }
h1 { font-size: 28px; color: #1a1a1a; margin-bottom: 4px; }
.title { color: #6b7280; font-size: 16px; }
.card { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
.card-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #1a1a1a; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px; }
.info-row { display: flex; align-items: center; padding: 12px; border-radius: 6px; margin-bottom: 8px; background: #f9fafb; }
.info-icon { font-size: 20px; margin-right: 12px; }
.info-content { flex: 1; }
.info-label { font-size: 11px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; }
.info-value { font-size: 15px; color: #1a1a1a; margin-top: 2px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="avatar">${contact.name ? contact.name.charAt(0).toUpperCase() : '?'}</div>
<h1>${contact.name || 'Unnamed Contact'}</h1>
${contact.title ? `<div class="title">${contact.title}</div>` : ''}
</div>
<div class="card">
<div class="card-title">Contact Information</div>
${contact.emails && contact.emails.length > 0 ? `
<div class="info-row">
<div class="info-icon">📧</div>
<div class="info-content">
<div class="info-label">Email Addresses</div>
${contact.emails.map((e: any) => `
<div class="info-value">${e.email}${e.type ? ` (${e.type})` : ''}</div>
`).join('')}
</div>
</div>
` : ''}
${contact.phones && contact.phones.length > 0 ? `
<div class="info-row">
<div class="info-icon">📞</div>
<div class="info-content">
<div class="info-label">Phone Numbers</div>
${contact.phones.map((p: any) => `
<div class="info-value">${p.phone}${p.type ? ` (${p.type})` : ''}</div>
`).join('')}
</div>
</div>
` : ''}
${contact.urls && contact.urls.length > 0 ? `
<div class="info-row">
<div class="info-icon">🔗</div>
<div class="info-content">
<div class="info-label">URLs</div>
${contact.urls.map((u: any) => `
<div class="info-value"><a href="${u.url}" target="_blank">${u.url}</a>${u.type ? ` (${u.type})` : ''}</div>
`).join('')}
</div>
</div>
` : ''}
</div>
<div class="card">
<div class="card-title">Metadata</div>
<div class="info-row">
<div class="info-icon">🆔</div>
<div class="info-content">
<div class="info-label">Contact ID</div>
<div class="info-value">${contact.id}</div>
</div>
</div>
${contact.lead_id ? `
<div class="info-row">
<div class="info-icon">🏢</div>
<div class="info-content">
<div class="info-label">Lead ID</div>
<div class="info-value">${contact.lead_id}</div>
</div>
</div>
` : ''}
<div class="info-row">
<div class="info-icon">📅</div>
<div class="info-content">
<div class="info-label">Created</div>
<div class="info-value">${contact.date_created ? new Date(contact.date_created).toLocaleString() : 'N/A'}</div>
</div>
</div>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,68 @@
// Custom Fields Manager MCP App
export function generateCustomFieldsManager(fields: any[]) {
const fieldsByType: Record<string, any[]> = {};
fields.forEach(field => {
const type = field.type || 'unknown';
if (!fieldsByType[type]) fieldsByType[type] = [];
fieldsByType[type].push(field);
});
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Fields Manager</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.section { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
.section-title { font-size: 20px; font-weight: 600; margin-bottom: 16px; color: #1a1a1a; display: flex; justify-content: space-between; align-items: center; }
.count { background: #3b82f6; color: white; padding: 4px 12px; border-radius: 12px; font-size: 14px; }
.fields-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.field-card { border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px; background: #f9fafb; }
.field-name { font-weight: 600; font-size: 16px; color: #1a1a1a; margin-bottom: 8px; }
.field-info { font-size: 13px; color: #6b7280; margin-bottom: 4px; }
.badge { display: inline-block; padding: 3px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; margin-right: 6px; }
.badge-required { background: #fee2e2; color: #991b1b; }
.badge-multiple { background: #dbeafe; color: #1e40af; }
.badge-type { background: #e5e7eb; color: #374151; }
</style>
</head>
<body>
<div class="container">
<h1> Custom Fields Manager</h1>
${Object.entries(fieldsByType).map(([type, typeFields]) => `
<div class="section">
<div class="section-title">
<span>${type.charAt(0).toUpperCase() + type.slice(1)} Fields</span>
<span class="count">${typeFields.length}</span>
</div>
<div class="fields-grid">
${typeFields.map(field => `
<div class="field-card">
<div class="field-name">${field.name}</div>
<div style="margin-bottom: 8px;">
<span class="badge badge-type">${field.type}</span>
${field.required ? '<span class="badge badge-required">Required</span>' : ''}
${field.accepts_multiple_values ? '<span class="badge badge-multiple">Multiple</span>' : ''}
</div>
<div class="field-info">ID: ${field.id}</div>
${field.choices && field.choices.length > 0 ? `
<div class="field-info">Choices: ${field.choices.join(', ')}</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,95 @@
// Email Log MCP App
export function generateEmailLog(emails: any[]) {
const totalEmails = emails.length;
const sent = emails.filter(e => e.direction === 'outbound').length;
const received = emails.filter(e => e.direction === 'inbound').length;
const totalOpens = emails.reduce((sum: number, e: any) => sum + (e.opens || 0), 0);
const totalClicks = emails.reduce((sum: number, e: any) => sum + (e.clicks || 0), 0);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Log</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-label { color: #6b7280; font-size: 13px; text-transform: uppercase; margin-bottom: 8px; }
.stat-value { font-size: 32px; font-weight: 600; color: #1a1a1a; }
.email-list { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.email-item { padding: 20px; border-bottom: 1px solid #e5e7eb; }
.email-item:last-child { border-bottom: none; }
.email-item:hover { background: #f9fafb; }
.email-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px; }
.email-subject { font-weight: 600; font-size: 16px; color: #1a1a1a; }
.email-date { font-size: 13px; color: #6b7280; }
.email-from { font-size: 14px; color: #6b7280; margin-bottom: 8px; }
.email-preview { font-size: 14px; color: #374151; line-height: 1.5; }
.email-meta { margin-top: 12px; display: flex; gap: 16px; font-size: 12px; color: #6b7280; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; }
.badge-sent { background: #dbeafe; color: #1e40af; }
.badge-received { background: #d1fae5; color: #065f46; }
</style>
</head>
<body>
<div class="container">
<h1>📧 Email Log</h1>
<div class="stats">
<div class="stat-card">
<div class="stat-label">Total Emails</div>
<div class="stat-value">${totalEmails}</div>
</div>
<div class="stat-card">
<div class="stat-label">Sent</div>
<div class="stat-value" style="color: #3b82f6;">${sent}</div>
</div>
<div class="stat-card">
<div class="stat-label">Received</div>
<div class="stat-value" style="color: #059669;">${received}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Opens</div>
<div class="stat-value" style="color: #8b5cf6;">${totalOpens}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Clicks</div>
<div class="stat-value" style="color: #f59e0b;">${totalClicks}</div>
</div>
</div>
<div class="email-list">
${emails.length > 0 ? emails.map((email: any) => `
<div class="email-item">
<div class="email-header">
<div class="email-subject">${email.subject || 'No Subject'}</div>
<div class="email-date">${email.date_created ? new Date(email.date_created).toLocaleString() : 'N/A'}</div>
</div>
<div class="email-from">
${email.direction === 'inbound' ? 'From' : 'To'}: ${email.sender || email.to?.[0] || 'Unknown'}
<span class="badge badge-${email.direction === 'outbound' ? 'sent' : 'received'}">${email.direction}</span>
</div>
${email.body_text ? `
<div class="email-preview">${email.body_text.substring(0, 200)}${email.body_text.length > 200 ? '...' : ''}</div>
` : ''}
<div class="email-meta">
${email.status ? `<span>Status: ${email.status}</span>` : ''}
${email.opens !== undefined ? `<span>📖 ${email.opens} opens</span>` : ''}
${email.clicks !== undefined ? `<span>🖱️ ${email.clicks} clicks</span>` : ''}
${email.user_name ? `<span>👤 ${email.user_name}</span>` : ''}
</div>
</div>
`).join('') : '<div style="padding: 40px; text-align: center; color: #6b7280;">No emails found</div>'}
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,102 @@
// Lead Dashboard MCP App
export function generateLeadDashboard(data: any) {
const leads = data.leads || [];
const statuses = data.statuses || [];
const statusCounts: Record<string, number> = {};
leads.forEach((lead: any) => {
const status = lead.status_label || "No Status";
statusCounts[status] = (statusCounts[status] || 0) + 1;
});
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Close CRM - Lead Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
.header { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
h1 { font-size: 28px; color: #1a1a1a; margin-bottom: 8px; }
.subtitle { color: #6b7280; font-size: 14px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-label { color: #6b7280; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
.stat-value { font-size: 32px; font-weight: 600; color: #1a1a1a; }
.chart-container { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
.chart-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #1a1a1a; }
.bar { height: 32px; background: linear-gradient(90deg, #3b82f6, #60a5fa); border-radius: 4px; margin-bottom: 12px; display: flex; align-items: center; padding: 0 12px; color: white; font-size: 13px; font-weight: 500; }
.leads-table { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; }
th { background: #f9fafb; padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; }
td { padding: 14px 16px; border-top: 1px solid #e5e7eb; font-size: 14px; }
tr:hover { background: #f9fafb; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.badge-new { background: #dbeafe; color: #1e40af; }
.badge-potential { background: #fef3c7; color: #92400e; }
.badge-qualified { background: #d1fae5; color: #065f46; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 Lead Dashboard</h1>
<div class="subtitle">Overview of all leads and statuses</div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-label">Total Leads</div>
<div class="stat-value">${leads.length}</div>
</div>
<div class="stat-card">
<div class="stat-label">Unique Statuses</div>
<div class="stat-value">${Object.keys(statusCounts).length}</div>
</div>
<div class="stat-card">
<div class="stat-label">Most Common</div>
<div class="stat-value" style="font-size: 18px;">${Object.entries(statusCounts).sort((a, b) => (b[1] as number) - (a[1] as number))[0]?.[0] || 'N/A'}</div>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Leads by Status</div>
${Object.entries(statusCounts).sort((a, b) => (b[1] as number) - (a[1] as number)).map(([status, count]) => `
<div class="bar" style="width: ${Math.max(20, (count as number / leads.length) * 100)}%">
${status}: ${count}
</div>
`).join('')}
</div>
<div class="leads-table">
<table>
<thead>
<tr>
<th>Lead Name</th>
<th>Status</th>
<th>Contacts</th>
<th>Created</th>
</tr>
</thead>
<tbody>
${leads.slice(0, 50).map((lead: any) => `
<tr>
<td><strong>${lead.name || lead.display_name}</strong></td>
<td><span class="badge badge-potential">${lead.status_label || 'No Status'}</span></td>
<td>${lead.contacts?.length || 0}</td>
<td>${lead.date_created ? new Date(lead.date_created).toLocaleDateString() : 'N/A'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,109 @@
// Lead Detail MCP App
export function generateLeadDetail(lead: any) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${lead.name} - Lead Detail</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.header { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
h1 { font-size: 32px; color: #1a1a1a; margin-bottom: 8px; }
.status-badge { display: inline-block; padding: 6px 14px; border-radius: 16px; font-size: 13px; font-weight: 500; background: #dbeafe; color: #1e40af; }
.grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
.card { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
.card-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #1a1a1a; }
.field { margin-bottom: 16px; }
.field-label { font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.field-value { font-size: 15px; color: #1a1a1a; }
.contact-card { border: 1px solid #e5e7eb; border-radius: 6px; padding: 14px; margin-bottom: 12px; }
.contact-name { font-weight: 600; margin-bottom: 4px; }
.contact-info { font-size: 13px; color: #6b7280; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${lead.name}</h1>
<span class="status-badge">${lead.status_label || 'No Status'}</span>
</div>
<div class="grid">
<div>
<div class="card">
<div class="card-title">Lead Information</div>
<div class="field">
<div class="field-label">Lead ID</div>
<div class="field-value">${lead.id}</div>
</div>
${lead.description ? `
<div class="field">
<div class="field-label">Description</div>
<div class="field-value">${lead.description}</div>
</div>
` : ''}
${lead.url ? `
<div class="field">
<div class="field-label">Website</div>
<div class="field-value"><a href="${lead.url}" target="_blank">${lead.url}</a></div>
</div>
` : ''}
<div class="field">
<div class="field-label">Created</div>
<div class="field-value">${lead.date_created ? new Date(lead.date_created).toLocaleString() : 'N/A'}</div>
</div>
<div class="field">
<div class="field-label">Last Updated</div>
<div class="field-value">${lead.date_updated ? new Date(lead.date_updated).toLocaleString() : 'N/A'}</div>
</div>
</div>
<div class="card">
<div class="card-title">Contacts (${lead.contacts?.length || 0})</div>
${lead.contacts && lead.contacts.length > 0 ? lead.contacts.map((contact: any) => `
<div class="contact-card">
<div class="contact-name">${contact.name || 'Unnamed Contact'}</div>
${contact.title ? `<div class="contact-info">${contact.title}</div>` : ''}
${contact.emails && contact.emails.length > 0 ? `<div class="contact-info">📧 ${contact.emails.map((e: any) => e.email).join(', ')}</div>` : ''}
${contact.phones && contact.phones.length > 0 ? `<div class="contact-info">📞 ${contact.phones.map((p: any) => p.phone).join(', ')}</div>` : ''}
</div>
`).join('') : '<p style="color: #6b7280;">No contacts</p>'}
</div>
</div>
<div>
<div class="card">
<div class="card-title">Quick Stats</div>
<div class="field">
<div class="field-label">Opportunities</div>
<div class="field-value" style="font-size: 28px; font-weight: 600;">${lead.opportunities?.length || 0}</div>
</div>
<div class="field">
<div class="field-label">Tasks</div>
<div class="field-value" style="font-size: 28px; font-weight: 600;">${lead.tasks?.length || 0}</div>
</div>
</div>
${lead.custom && Object.keys(lead.custom).length > 0 ? `
<div class="card">
<div class="card-title">Custom Fields</div>
${Object.entries(lead.custom).map(([key, value]) => `
<div class="field">
<div class="field-label">${key}</div>
<div class="field-value">${value}</div>
</div>
`).join('')}
</div>
` : ''}
</div>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,49 @@
// Lead Grid MCP App
export function generateLeadGrid(leads: any[]) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lead Grid</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1600px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: transform 0.2s, box-shadow 0.2s; cursor: pointer; }
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.card-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px; }
.card-title { font-size: 18px; font-weight: 600; color: #1a1a1a; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; background: #dbeafe; color: #1e40af; }
.card-info { font-size: 13px; color: #6b7280; margin-bottom: 8px; }
.card-meta { display: flex; gap: 16px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #6b7280; }
</style>
</head>
<body>
<div class="container">
<h1>📇 Lead Grid (${leads.length})</h1>
<div class="grid">
${leads.map(lead => `
<div class="card">
<div class="card-header">
<div class="card-title">${lead.name}</div>
<span class="badge">${lead.status_label || 'No Status'}</span>
</div>
${lead.description ? `<div class="card-info">${lead.description.substring(0, 100)}${lead.description.length > 100 ? '...' : ''}</div>` : ''}
${lead.url ? `<div class="card-info">🌐 ${lead.url}</div>` : ''}
<div class="card-meta">
<div>👤 ${lead.contacts?.length || 0} contacts</div>
<div>💼 ${lead.opportunities?.length || 0} opps</div>
</div>
</div>
`).join('')}
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,92 @@
// Opportunity Dashboard MCP App
export function generateOpportunityDashboard(data: any) {
const opportunities = data.opportunities || [];
const totalValue = opportunities.reduce((sum: number, opp: any) => sum + (opp.value || 0), 0);
const avgValue = opportunities.length > 0 ? totalValue / opportunities.length : 0;
const wonOpps = opportunities.filter((o: any) => o.status_type === 'won');
const wonValue = wonOpps.reduce((sum: number, opp: any) => sum + (opp.value || 0), 0);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opportunity Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-label { color: #6b7280; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
.stat-value { font-size: 36px; font-weight: 600; color: #1a1a1a; }
.stat-subtext { color: #6b7280; font-size: 13px; margin-top: 4px; }
.table-container { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; }
th { background: #f9fafb; padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; }
td { padding: 14px 16px; border-top: 1px solid #e5e7eb; font-size: 14px; }
tr:hover { background: #f9fafb; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.badge-won { background: #d1fae5; color: #065f46; }
.badge-active { background: #dbeafe; color: #1e40af; }
.badge-lost { background: #fee2e2; color: #991b1b; }
</style>
</head>
<body>
<div class="container">
<h1>💼 Opportunity Dashboard</h1>
<div class="stats">
<div class="stat-card">
<div class="stat-label">Total Opportunities</div>
<div class="stat-value">${opportunities.length}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Value</div>
<div class="stat-value">$${totalValue.toLocaleString()}</div>
<div class="stat-subtext">Across all opportunities</div>
</div>
<div class="stat-card">
<div class="stat-label">Average Value</div>
<div class="stat-value">$${Math.round(avgValue).toLocaleString()}</div>
</div>
<div class="stat-card">
<div class="stat-label">Won Revenue</div>
<div class="stat-value" style="color: #059669;">$${wonValue.toLocaleString()}</div>
<div class="stat-subtext">${wonOpps.length} won opportunities</div>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Lead</th>
<th>Value</th>
<th>Status</th>
<th>Confidence</th>
<th>Created</th>
</tr>
</thead>
<tbody>
${opportunities.slice(0, 50).map((opp: any) => `
<tr>
<td><strong>${opp.lead_name || opp.lead_id}</strong></td>
<td>$${(opp.value || 0).toLocaleString()}${opp.value_period ? ` /${opp.value_period}` : ''}</td>
<td><span class="badge badge-${opp.status_type === 'won' ? 'won' : opp.status_type === 'lost' ? 'lost' : 'active'}">${opp.status_label || opp.status_type || 'Unknown'}</span></td>
<td>${opp.confidence !== undefined ? opp.confidence + '%' : 'N/A'}</td>
<td>${opp.date_created ? new Date(opp.date_created).toLocaleDateString() : 'N/A'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,96 @@
// Opportunity Detail MCP App
export function generateOpportunityDetail(opportunity: any) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opportunity Detail</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1000px; margin: 0 auto; }
.header { background: white; padding: 32px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.value { font-size: 48px; font-weight: 700; color: #059669; margin-bottom: 8px; }
.status { display: inline-block; padding: 6px 14px; border-radius: 16px; font-size: 14px; font-weight: 500; background: #dbeafe; color: #1e40af; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.card { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #1a1a1a; }
.field { margin-bottom: 16px; }
.field-label { font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.field-value { font-size: 15px; color: #1a1a1a; }
.confidence-bar { height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; margin-top: 8px; }
.confidence-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #60a5fa); transition: width 0.3s; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="value">$${(opportunity.value || 0).toLocaleString()}</div>
<span class="status">${opportunity.status_label || opportunity.status_type || 'Unknown Status'}</span>
${opportunity.value_period ? `<div style="color: #6b7280; margin-top: 8px; font-size: 14px;">per ${opportunity.value_period}</div>` : ''}
</div>
<div class="grid">
<div class="card">
<div class="card-title">Opportunity Details</div>
<div class="field">
<div class="field-label">Opportunity ID</div>
<div class="field-value">${opportunity.id}</div>
</div>
<div class="field">
<div class="field-label">Lead ID</div>
<div class="field-value">${opportunity.lead_id}</div>
</div>
<div class="field">
<div class="field-label">Status ID</div>
<div class="field-value">${opportunity.status_id}</div>
</div>
${opportunity.user_name ? `
<div class="field">
<div class="field-label">Owner</div>
<div class="field-value">${opportunity.user_name}</div>
</div>
` : ''}
${opportunity.note ? `
<div class="field">
<div class="field-label">Notes</div>
<div class="field-value">${opportunity.note}</div>
</div>
` : ''}
</div>
<div class="card">
<div class="card-title">Metrics</div>
<div class="field">
<div class="field-label">Confidence</div>
<div class="field-value">${opportunity.confidence !== undefined ? opportunity.confidence + '%' : 'Not set'}</div>
${opportunity.confidence !== undefined ? `
<div class="confidence-bar">
<div class="confidence-fill" style="width: ${opportunity.confidence}%"></div>
</div>
` : ''}
</div>
<div class="field">
<div class="field-label">Created</div>
<div class="field-value">${opportunity.date_created ? new Date(opportunity.date_created).toLocaleString() : 'N/A'}</div>
</div>
<div class="field">
<div class="field-label">Last Updated</div>
<div class="field-value">${opportunity.date_updated ? new Date(opportunity.date_updated).toLocaleString() : 'N/A'}</div>
</div>
${opportunity.date_won ? `
<div class="field">
<div class="field-label">Won Date</div>
<div class="field-value">${new Date(opportunity.date_won).toLocaleString()}</div>
</div>
` : ''}
</div>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,70 @@
// Pipeline Funnel MCP App
export function generatePipelineFunnel(data: any) {
const pipeline = data.pipeline || {};
const opportunities = data.opportunities || [];
const statuses = pipeline.statuses || [];
const funnelData = statuses.map((status: any) => {
const opps = opportunities.filter((opp: any) => opp.status_id === status.id);
const totalValue = opps.reduce((sum: number, opp: any) => sum + (opp.value || 0), 0);
return {
label: status.label,
count: opps.length,
value: totalValue,
};
});
const maxCount = Math.max(...funnelData.map((d) => d.count), 1);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${pipeline.name || 'Pipeline'} - Funnel</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1000px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.funnel { background: white; padding: 32px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.funnel-stage { margin-bottom: 20px; }
.funnel-bar-container { position: relative; }
.funnel-bar { height: 80px; background: linear-gradient(90deg, #3b82f6, #60a5fa); border-radius: 8px; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; color: white; font-weight: 600; transition: all 0.3s; }
.funnel-bar:hover { transform: scale(1.02); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); }
.funnel-label { font-size: 18px; }
.funnel-stats { text-align: right; }
.funnel-count { font-size: 28px; margin-bottom: 2px; }
.funnel-value { font-size: 14px; opacity: 0.9; }
</style>
</head>
<body>
<div class="container">
<h1>🔄 ${pipeline.name || 'Pipeline Funnel'}</h1>
<div class="funnel">
${funnelData.map((stage, index) => {
const widthPercent = Math.max(30, (stage.count / maxCount) * 100);
const hue = 210 + (index * 15);
return `
<div class="funnel-stage">
<div class="funnel-bar-container">
<div class="funnel-bar" style="width: ${widthPercent}%; background: linear-gradient(90deg, hsl(${hue}, 75%, 55%), hsl(${hue}, 75%, 65%));">
<div class="funnel-label">${stage.label}</div>
<div class="funnel-stats">
<div class="funnel-count">${stage.count}</div>
<div class="funnel-value">$${stage.value.toLocaleString()}</div>
</div>
</div>
</div>
</div>
`;
}).join('')}
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,74 @@
// Pipeline Kanban MCP App
export function generatePipelineKanban(data: any) {
const pipeline = data.pipeline || {};
const opportunities = data.opportunities || [];
const statuses = pipeline.statuses || [];
const oppsByStatus: Record<string, any[]> = {};
statuses.forEach((status: any) => {
oppsByStatus[status.id] = opportunities.filter(
(opp: any) => opp.status_id === status.id
);
});
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${pipeline.name || 'Pipeline'} - Kanban</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; overflow-x: auto; }
.container { min-width: 1200px; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.board { display: flex; gap: 16px; padding-bottom: 20px; }
.column { background: #e5e7eb; border-radius: 8px; padding: 16px; min-width: 280px; flex-shrink: 0; }
.column-header { font-size: 14px; font-weight: 600; color: #1a1a1a; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center; }
.column-count { background: #6b7280; color: white; padding: 2px 8px; border-radius: 10px; font-size: 12px; }
.card { background: white; border-radius: 6px; padding: 14px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer; transition: transform 0.2s; }
.card:hover { transform: translateY(-2px); box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
.card-title { font-weight: 600; margin-bottom: 8px; font-size: 14px; }
.card-value { font-size: 18px; font-weight: 600; color: #059669; margin-bottom: 8px; }
.card-meta { font-size: 12px; color: #6b7280; }
</style>
</head>
<body>
<div class="container">
<h1>📊 ${pipeline.name || 'Pipeline Kanban'}</h1>
<div class="board">
${statuses.map((status: any) => {
const opps = oppsByStatus[status.id] || [];
const totalValue = opps.reduce((sum: number, opp: any) => sum + (opp.value || 0), 0);
return `
<div class="column">
<div class="column-header">
<span>${status.label}</span>
<span class="column-count">${opps.length}</span>
</div>
${opps.map((opp: any) => `
<div class="card">
<div class="card-title">${opp.lead_name || opp.lead_id}</div>
<div class="card-value">$${(opp.value || 0).toLocaleString()}</div>
<div class="card-meta">
${opp.confidence !== undefined ? `${opp.confidence}% confidence` : 'No confidence set'}
</div>
</div>
`).join('')}
${opps.length > 0 ? `
<div style="margin-top: 12px; padding-top: 12px; border-top: 2px solid #d1d5db; font-size: 13px; font-weight: 600; color: #374151;">
Total: $${totalValue.toLocaleString()}
</div>
` : '<div style="color: #9ca3af; font-size: 13px; text-align: center; padding: 20px;">No opportunities</div>'}
</div>
`;
}).join('')}
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,99 @@
// Report Builder MCP App
export function generateReportBuilder(data: any) {
const reports = data.reports || [];
const templates = [
{ name: 'Lead Status Changes', type: 'lead_status_changes' },
{ name: 'Opportunity Funnel', type: 'opportunity_funnel' },
{ name: 'Activity Overview', type: 'activity_overview' },
{ name: 'Revenue Forecast', type: 'revenue_forecast' },
{ name: 'User Leaderboard', type: 'leaderboard' },
];
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report Builder</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.grid { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; }
.card { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #1a1a1a; }
.template { padding: 14px; border: 1px solid #e5e7eb; border-radius: 6px; margin-bottom: 10px; cursor: pointer; transition: all 0.2s; }
.template:hover { background: #f9fafb; border-color: #3b82f6; }
.template-name { font-weight: 600; margin-bottom: 4px; }
.template-type { font-size: 12px; color: #6b7280; }
.form-group { margin-bottom: 20px; }
.label { font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 6px; display: block; }
.input { width: 100%; padding: 10px 14px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }
.btn { padding: 12px 24px; background: #3b82f6; color: white; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; }
.btn:hover { background: #2563eb; }
</style>
</head>
<body>
<div class="container">
<h1>📊 Report Builder</h1>
<div class="grid">
<div class="card">
<div class="card-title">Report Templates</div>
${templates.map(template => `
<div class="template">
<div class="template-name">${template.name}</div>
<div class="template-type">${template.type}</div>
</div>
`).join('')}
</div>
<div class="card">
<div class="card-title">Build Your Report</div>
<div class="form-group">
<label class="label">Report Type</label>
<select class="input">
<option>Select report type...</option>
${templates.map(t => `<option value="${t.type}">${t.name}</option>`).join('')}
</select>
</div>
<div class="form-group">
<label class="label">Date Range</label>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<input type="date" class="input" placeholder="Start Date">
<input type="date" class="input" placeholder="End Date">
</div>
</div>
<div class="form-group">
<label class="label">Filter By User (Optional)</label>
<input type="text" class="input" placeholder="User ID">
</div>
<div class="form-group">
<label class="label">Filter By Pipeline (Optional)</label>
<input type="text" class="input" placeholder="Pipeline ID">
</div>
<button class="btn">🚀 Generate Report</button>
</div>
</div>
${reports.length > 0 ? `
<div class="card" style="margin-top: 20px;">
<div class="card-title">Recent Reports</div>
<div style="color: #6b7280; font-size: 14px;">
${reports.length} reports generated
</div>
</div>
` : ''}
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,97 @@
// Revenue Dashboard MCP App
export function generateRevenueDashboard(data: any) {
const opportunities = data.opportunities || [];
const wonOpps = opportunities.filter((o: any) => o.status_type === 'won');
const activeOpps = opportunities.filter((o: any) => o.status_type === 'active');
const totalRevenue = wonOpps.reduce((sum: number, o: any) => sum + (o.value || 0), 0);
const pipelineValue = activeOpps.reduce((sum: number, o: any) => sum + (o.value || 0), 0);
const weightedValue = activeOpps.reduce((sum: number, o: any) => {
return sum + ((o.value || 0) * ((o.confidence || 0) / 100));
}, 0);
// Group by month
const revenueByMonth: Record<string, number> = {};
wonOpps.forEach((opp: any) => {
if (opp.date_won) {
const month = new Date(opp.date_won).toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
revenueByMonth[month] = (revenueByMonth[month] || 0) + (opp.value || 0);
}
});
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Revenue Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; padding: 28px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-label { color: #6b7280; font-size: 14px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
.stat-value { font-size: 42px; font-weight: 700; margin-bottom: 4px; }
.stat-subtext { color: #6b7280; font-size: 13px; }
.chart { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.chart-title { font-size: 20px; font-weight: 600; margin-bottom: 20px; color: #1a1a1a; }
.bar-container { margin-bottom: 16px; }
.bar-label { font-size: 13px; color: #6b7280; margin-bottom: 6px; }
.bar-wrapper { display: flex; gap: 12px; align-items: center; }
.bar { height: 36px; background: linear-gradient(90deg, #059669, #10b981); border-radius: 4px; display: flex; align-items: center; padding: 0 12px; color: white; font-weight: 600; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<h1>💰 Revenue Dashboard</h1>
<div class="stats">
<div class="stat-card">
<div class="stat-label">Total Revenue</div>
<div class="stat-value" style="color: #059669;">$${totalRevenue.toLocaleString()}</div>
<div class="stat-subtext">${wonOpps.length} won opportunities</div>
</div>
<div class="stat-card">
<div class="stat-label">Pipeline Value</div>
<div class="stat-value" style="color: #3b82f6;">$${pipelineValue.toLocaleString()}</div>
<div class="stat-subtext">${activeOpps.length} active opportunities</div>
</div>
<div class="stat-card">
<div class="stat-label">Weighted Pipeline</div>
<div class="stat-value" style="color: #8b5cf6;">$${Math.round(weightedValue).toLocaleString()}</div>
<div class="stat-subtext">Based on confidence</div>
</div>
<div class="stat-card">
<div class="stat-label">Average Deal Size</div>
<div class="stat-value" style="color: #f59e0b;">$${wonOpps.length > 0 ? Math.round(totalRevenue / wonOpps.length).toLocaleString() : '0'}</div>
<div class="stat-subtext">Won opportunities</div>
</div>
</div>
<div class="chart">
<div class="chart-title">Revenue by Month</div>
${Object.entries(revenueByMonth).length > 0 ? Object.entries(revenueByMonth).map(([month, value]) => {
const maxValue = Math.max(...Object.values(revenueByMonth));
const widthPercent = Math.max(20, (value / maxValue) * 100);
return `
<div class="bar-container">
<div class="bar-label">${month}</div>
<div class="bar-wrapper">
<div class="bar" style="width: ${widthPercent}%">
$${value.toLocaleString()}
</div>
</div>
</div>
`;
}).join('') : '<p style="color: #6b7280; text-align: center; padding: 40px;">No revenue data available</p>'}
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,73 @@
// Search Results MCP App
export function generateSearchResults(data: any) {
const query = data.query || '';
const results = data.results || [];
const resultType = data.resultType || 'all';
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search Results</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.search-header { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
h1 { font-size: 24px; color: #1a1a1a; margin-bottom: 12px; }
.search-meta { color: #6b7280; font-size: 14px; }
.query { font-weight: 600; color: #3b82f6; }
.results-container { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.result { padding: 20px; border-bottom: 1px solid #e5e7eb; }
.result:last-child { border-bottom: none; }
.result:hover { background: #f9fafb; }
.result-title { font-size: 18px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.result-desc { font-size: 14px; color: #6b7280; margin-bottom: 12px; line-height: 1.5; }
.result-meta { display: flex; gap: 16px; font-size: 12px; color: #6b7280; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; background: #dbeafe; color: #1e40af; }
.no-results { padding: 60px 20px; text-align: center; color: #6b7280; }
.no-results-icon { font-size: 48px; margin-bottom: 16px; }
</style>
</head>
<body>
<div class="container">
<div class="search-header">
<h1>Search Results</h1>
<div class="search-meta">
Found <strong>${results.length}</strong> results for <span class="query">"${query}"</span>
</div>
</div>
<div class="results-container">
${results.length > 0 ? results.map((result: any) => `
<div class="result">
<div class="result-title">
${result.name || result.display_name || result.text || 'Untitled'}
<span class="badge">${result._type || resultType}</span>
</div>
${result.description ? `<div class="result-desc">${result.description}</div>` : ''}
${result.note ? `<div class="result-desc">${result.note}</div>` : ''}
<div class="result-meta">
${result.id ? `<span>ID: ${result.id}</span>` : ''}
${result.status_label ? `<span>Status: ${result.status_label}</span>` : ''}
${result.date_created ? `<span>Created: ${new Date(result.date_created).toLocaleDateString()}</span>` : ''}
${result.contacts ? `<span>${result.contacts.length} contacts</span>` : ''}
${result.value ? `<span>$${result.value.toLocaleString()}</span>` : ''}
</div>
</div>
`).join('') : `
<div class="no-results">
<div class="no-results-icon">🔍</div>
<div style="font-size: 18px; font-weight: 600; margin-bottom: 8px;">No results found</div>
<div>Try adjusting your search query</div>
</div>
`}
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,64 @@
// Sequence Dashboard MCP App
export function generateSequenceDashboard(sequences: any[]) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sequence Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; }
.card { background: white; border-radius: 8px; padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: transform 0.2s; }
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.card-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px; }
.card-title { font-size: 20px; font-weight: 600; color: #1a1a1a; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.badge-active { background: #d1fae5; color: #065f46; }
.badge-paused { background: #fef3c7; color: #92400e; }
.badge-draft { background: #e5e7eb; color: #374151; }
.stat-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #e5e7eb; }
.stat-row:last-child { border-bottom: none; }
.stat-label { color: #6b7280; font-size: 13px; }
.stat-value { font-weight: 600; color: #1a1a1a; }
</style>
</head>
<body>
<div class="container">
<h1>📨 Sequence Dashboard (${sequences.length})</h1>
<div class="grid">
${sequences.map(seq => `
<div class="card">
<div class="card-header">
<div class="card-title">${seq.name}</div>
<span class="badge badge-${seq.status === 'active' ? 'active' : seq.status === 'paused' ? 'paused' : 'draft'}">
${seq.status || 'draft'}
</span>
</div>
<div class="stat-row">
<div class="stat-label">Sequence ID</div>
<div class="stat-value">${seq.id}</div>
</div>
${seq.max_activations ? `
<div class="stat-row">
<div class="stat-label">Max Activations</div>
<div class="stat-value">${seq.max_activations}</div>
</div>
` : ''}
<div class="stat-row">
<div class="stat-label">Created</div>
<div class="stat-value">${seq.date_created ? new Date(seq.date_created).toLocaleDateString() : 'N/A'}</div>
</div>
</div>
`).join('')}
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,93 @@
// Sequence Detail MCP App
export function generateSequenceDetail(data: any) {
const sequence = data.sequence || {};
const stats = data.stats || {};
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${sequence.name} - Sequence</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.header { background: white; padding: 32px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
h1 { font-size: 32px; color: #1a1a1a; margin-bottom: 8px; }
.status-badge { display: inline-block; padding: 6px 14px; border-radius: 16px; font-size: 13px; font-weight: 500; background: #d1fae5; color: #065f46; }
.grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
.card { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
.card-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #1a1a1a; }
.field { margin-bottom: 16px; }
.field-label { font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.field-value { font-size: 15px; color: #1a1a1a; }
.stat-big { text-align: center; padding: 20px; }
.stat-big-value { font-size: 48px; font-weight: 700; color: #3b82f6; margin-bottom: 8px; }
.stat-big-label { font-size: 14px; color: #6b7280; text-transform: uppercase; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${sequence.name}</h1>
<span class="status-badge">${sequence.status || 'Draft'}</span>
</div>
<div class="grid">
<div>
<div class="card">
<div class="card-title">Sequence Details</div>
<div class="field">
<div class="field-label">Sequence ID</div>
<div class="field-value">${sequence.id}</div>
</div>
${sequence.max_activations ? `
<div class="field">
<div class="field-label">Max Activations</div>
<div class="field-value">${sequence.max_activations}</div>
</div>
` : ''}
${sequence.throttle_capacity ? `
<div class="field">
<div class="field-label">Throttle Capacity</div>
<div class="field-value">${sequence.throttle_capacity} per ${sequence.throttle_period_seconds}s</div>
</div>
` : ''}
<div class="field">
<div class="field-label">Created</div>
<div class="field-value">${sequence.date_created ? new Date(sequence.date_created).toLocaleString() : 'N/A'}</div>
</div>
<div class="field">
<div class="field-label">Last Updated</div>
<div class="field-value">${sequence.date_updated ? new Date(sequence.date_updated).toLocaleString() : 'N/A'}</div>
</div>
</div>
</div>
<div>
${stats.total_subscriptions !== undefined ? `
<div class="card">
<div class="stat-big">
<div class="stat-big-value">${stats.total_subscriptions || 0}</div>
<div class="stat-big-label">Total Subscriptions</div>
</div>
</div>
` : ''}
${stats.active_subscriptions !== undefined ? `
<div class="card">
<div class="stat-big">
<div class="stat-big-value" style="color: #059669;">${stats.active_subscriptions || 0}</div>
<div class="stat-big-label">Active</div>
</div>
</div>
` : ''}
</div>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,76 @@
// Smart View Runner MCP App
export function generateSmartViewRunner(data: any) {
const view = data.view || {};
const results = data.results || [];
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${view.name} - Smart View</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
.header { background: white; padding: 24px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
h1 { font-size: 28px; color: #1a1a1a; margin-bottom: 8px; }
.query { background: #f9fafb; padding: 12px 16px; border-radius: 6px; font-family: 'Monaco', 'Courier New', monospace; font-size: 13px; color: #374151; margin-top: 12px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-label { color: #6b7280; font-size: 13px; text-transform: uppercase; margin-bottom: 8px; }
.stat-value { font-size: 32px; font-weight: 600; color: #1a1a1a; }
.results-table { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; }
th { background: #f9fafb; padding: 12px 16px; text-align: left; font-size: 12px; font-weight: 600; color: #6b7280; text-transform: uppercase; }
td { padding: 14px 16px; border-top: 1px solid #e5e7eb; font-size: 14px; }
tr:hover { background: #f9fafb; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔍 ${view.name || 'Smart View'}</h1>
${view.query ? `<div class="query">${view.query}</div>` : ''}
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-label">Total Results</div>
<div class="stat-value">${results.length}</div>
</div>
<div class="stat-card">
<div class="stat-label">View Type</div>
<div class="stat-value" style="font-size: 20px;">${view.type || 'Mixed'}</div>
</div>
</div>
<div class="results-table">
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Contacts</th>
<th>Created</th>
</tr>
</thead>
<tbody>
${results.length > 0 ? results.map((item: any) => `
<tr>
<td><strong>${item.name || item.display_name || 'N/A'}</strong></td>
<td>${item.status_label || 'N/A'}</td>
<td>${item.contacts?.length || 0}</td>
<td>${item.date_created ? new Date(item.date_created).toLocaleDateString() : 'N/A'}</td>
</tr>
`).join('') : '<tr><td colspan="4" style="text-align: center; padding: 40px; color: #6b7280;">No results found</td></tr>'}
</tbody>
</table>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,121 @@
// Task Manager MCP App
export function generateTaskManager(tasks: any[]) {
const incompleteTasks = tasks.filter(t => !t.is_complete);
const completedTasks = tasks.filter(t => t.is_complete);
const overdueTasks = incompleteTasks.filter(t => {
if (!t.date) return false;
return new Date(t.date) < new Date();
});
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1a1a1a; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-label { color: #6b7280; font-size: 13px; text-transform: uppercase; margin-bottom: 8px; }
.stat-value { font-size: 32px; font-weight: 600; }
.section { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; }
.section-title { font-size: 20px; font-weight: 600; margin-bottom: 16px; color: #1a1a1a; }
.task { display: flex; align-items: start; gap: 12px; padding: 14px; border-radius: 6px; margin-bottom: 10px; background: #f9fafb; }
.task:hover { background: #f3f4f6; }
.checkbox { width: 20px; height: 20px; border: 2px solid #d1d5db; border-radius: 4px; flex-shrink: 0; margin-top: 2px; }
.checkbox.checked { background: #059669; border-color: #059669; position: relative; }
.checkbox.checked::after { content: '✓'; position: absolute; color: white; font-size: 14px; top: -2px; left: 2px; }
.task-content { flex: 1; }
.task-text { font-size: 15px; color: #1a1a1a; margin-bottom: 4px; }
.task-meta { font-size: 12px; color: #6b7280; }
.task-overdue { border-left: 3px solid #dc2626; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; background: #fee2e2; color: #991b1b; margin-left: 8px; }
</style>
</head>
<body>
<div class="container">
<h1> Task Manager</h1>
<div class="stats">
<div class="stat-card">
<div class="stat-label">Total Tasks</div>
<div class="stat-value">${tasks.length}</div>
</div>
<div class="stat-card">
<div class="stat-label">Incomplete</div>
<div class="stat-value" style="color: #3b82f6;">${incompleteTasks.length}</div>
</div>
<div class="stat-card">
<div class="stat-label">Completed</div>
<div class="stat-value" style="color: #059669;">${completedTasks.length}</div>
</div>
<div class="stat-card">
<div class="stat-label">Overdue</div>
<div class="stat-value" style="color: #dc2626;">${overdueTasks.length}</div>
</div>
</div>
${overdueTasks.length > 0 ? `
<div class="section">
<div class="section-title">🚨 Overdue Tasks</div>
${overdueTasks.map(task => `
<div class="task task-overdue">
<div class="checkbox"></div>
<div class="task-content">
<div class="task-text">${task.text}</div>
<div class="task-meta">
${task.assigned_to_name ? `Assigned to ${task.assigned_to_name}` : 'Unassigned'}
${task.date ? ` • Due ${new Date(task.date).toLocaleDateString()}` : ''}
${task.lead_name ? ` • Lead: ${task.lead_name}` : ''}
</div>
</div>
</div>
`).join('')}
</div>
` : ''}
<div class="section">
<div class="section-title">📝 Incomplete Tasks</div>
${incompleteTasks.filter(t => !overdueTasks.includes(t)).length > 0 ? incompleteTasks.filter(t => !overdueTasks.includes(t)).map(task => `
<div class="task">
<div class="checkbox"></div>
<div class="task-content">
<div class="task-text">${task.text}</div>
<div class="task-meta">
${task.assigned_to_name ? `Assigned to ${task.assigned_to_name}` : 'Unassigned'}
${task.date ? ` • Due ${new Date(task.date).toLocaleDateString()}` : ''}
${task.lead_name ? ` • Lead: ${task.lead_name}` : ''}
</div>
</div>
</div>
`).join('') : '<p style="color: #6b7280;">No incomplete tasks</p>'}
</div>
${completedTasks.length > 0 ? `
<div class="section">
<div class="section-title"> Completed Tasks</div>
${completedTasks.slice(0, 20).map(task => `
<div class="task" style="opacity: 0.6;">
<div class="checkbox checked"></div>
<div class="task-content">
<div class="task-text" style="text-decoration: line-through;">${task.text}</div>
<div class="task-meta">
${task.assigned_to_name ? `Assigned to ${task.assigned_to_name}` : 'Unassigned'}
${task.lead_name ? ` • Lead: ${task.lead_name}` : ''}
</div>
</div>
</div>
`).join('')}
</div>
` : ''}
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,76 @@
// User Stats MCP App
export function generateUserStats(data: any) {
const user = data.user || {};
const stats = data.stats || {};
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${user.first_name || 'User'} ${user.last_name || ''} - Stats</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.header { background: white; padding: 32px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center; }
.avatar { width: 80px; height: 80px; border-radius: 50%; margin: 0 auto 16px; overflow: hidden; background: #e5e7eb; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
h1 { font-size: 28px; color: #1a1a1a; margin-bottom: 4px; }
.email { color: #6b7280; font-size: 14px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
.stat-card { background: white; padding: 24px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: center; }
.stat-icon { font-size: 32px; margin-bottom: 12px; }
.stat-value { font-size: 36px; font-weight: 700; color: #1a1a1a; margin-bottom: 4px; }
.stat-label { font-size: 13px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="avatar">
${user.image ? `<img src="${user.image}" alt="Avatar">` : '<div style="width: 100%; height: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 32px; font-weight: 600;">${user.first_name ? user.first_name.charAt(0) : '?'}</div>'}
</div>
<h1>${user.first_name || ''} ${user.last_name || ''}</h1>
<div class="email">${user.email || ''}</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">📞</div>
<div class="stat-value">${stats.calls || 0}</div>
<div class="stat-label">Calls</div>
</div>
<div class="stat-card">
<div class="stat-icon">📧</div>
<div class="stat-value">${stats.emails || 0}</div>
<div class="stat-label">Emails</div>
</div>
<div class="stat-card">
<div class="stat-icon">📅</div>
<div class="stat-value">${stats.meetings || 0}</div>
<div class="stat-label">Meetings</div>
</div>
<div class="stat-card">
<div class="stat-icon">💼</div>
<div class="stat-value">${stats.opportunities || 0}</div>
<div class="stat-label">Opportunities</div>
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-value">${stats.tasks_completed || 0}</div>
<div class="stat-label">Tasks Done</div>
</div>
<div class="stat-card">
<div class="stat-icon">💰</div>
<div class="stat-value">$${(stats.revenue || 0).toLocaleString()}</div>
<div class="stat-label">Revenue</div>
</div>
</div>
</div>
</body>
</html>
`.trim();
}

View File

@ -0,0 +1,183 @@
// Close API Client with authentication, pagination, and error handling
import type {
CloseConfig,
PaginationParams,
SearchParams,
CloseAPIResponse,
} from "../types/index.js";
export class CloseClient {
private apiKey: string;
private baseUrl: string;
private authHeader: string;
constructor(config: CloseConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || "https://api.close.com/api/v1";
// Basic auth: encode "api_key:" (note the colon after key, no password)
this.authHeader = `Basic ${Buffer.from(`${this.apiKey}:`).toString("base64")}`;
}
/**
* Make a GET request to the Close API
*/
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
const url = this.buildUrl(endpoint, params);
return this.request<T>("GET", url);
}
/**
* Make a POST request to the Close API
*/
async post<T>(endpoint: string, body?: any): Promise<T> {
const url = this.buildUrl(endpoint);
return this.request<T>("POST", url, body);
}
/**
* Make a PUT request to the Close API
*/
async put<T>(endpoint: string, body?: any): Promise<T> {
const url = this.buildUrl(endpoint);
return this.request<T>("PUT", url, body);
}
/**
* Make a DELETE request to the Close API
*/
async delete<T>(endpoint: string): Promise<T> {
const url = this.buildUrl(endpoint);
return this.request<T>("DELETE", url);
}
/**
* Paginated GET request - fetches all pages automatically
*/
async getPaginated<T>(
endpoint: string,
params?: PaginationParams & Record<string, any>
): Promise<T[]> {
const results: T[] = [];
let cursor: string | undefined;
const limit = params?.limit || 100;
do {
const queryParams = {
...params,
_limit: limit,
_skip: cursor ? undefined : params?.skip,
_cursor: cursor,
};
const response = await this.get<CloseAPIResponse<T>>(
endpoint,
queryParams
);
if (response.data) {
results.push(...response.data);
}
cursor = response.has_more ? response.cursor : undefined;
} while (cursor);
return results;
}
/**
* Search with pagination support
*/
async search<T>(
endpoint: string,
params?: SearchParams
): Promise<T[]> {
return this.getPaginated<T>(endpoint, params);
}
/**
* Build URL with query parameters
*/
private buildUrl(
endpoint: string,
params?: Record<string, any>
): string {
const url = new URL(`${this.baseUrl}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
value.forEach((v) => url.searchParams.append(key, String(v)));
} else {
url.searchParams.set(key, String(value));
}
}
});
}
return url.toString();
}
/**
* Make HTTP request with error handling
*/
private async request<T>(
method: string,
url: string,
body?: any
): Promise<T> {
try {
const headers: Record<string, string> = {
Authorization: this.authHeader,
"Content-Type": "application/json",
};
const options: RequestInit = {
method,
headers,
};
if (body && (method === "POST" || method === "PUT")) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
// Handle different response codes
if (response.status === 204) {
// No content - successful DELETE
return {} as T;
}
const data = await response.json();
if (!response.ok) {
throw new Error(
`Close API error (${response.status}): ${
data.error || data.errors?.[0] || "Unknown error"
}`
);
}
return data as T;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Close API request failed: ${error.message}`);
}
throw error;
}
}
/**
* Test the API connection
*/
async testConnection(): Promise<boolean> {
try {
await this.get("/me/");
return true;
} catch {
return false;
}
}
}

View File

@ -1,476 +0,0 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// ============================================
// CONFIGURATION
// ============================================
const MCP_NAME = "close";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://api.close.com/api/v1";
// ============================================
// REST API CLIENT
// ============================================
class CloseClient {
private apiKey: string;
private baseUrl: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
this.baseUrl = API_BASE_URL;
}
private getAuthHeader(): string {
// Close uses Basic auth with API key as username, empty password
return `Basic ${Buffer.from(`${this.apiKey}:`).toString('base64')}`;
}
async request(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Authorization": this.getAuthHeader(),
"Content-Type": "application/json",
"Accept": "application/json",
...options.headers,
},
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Close API error: ${response.status} ${response.statusText} - ${errorBody}`);
}
return response.json();
}
async get(endpoint: string, params: Record<string, any> = {}) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
}
const queryString = searchParams.toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
return this.request(url, { method: "GET" });
}
async post(endpoint: string, data: any) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
async put(endpoint: string, data: any) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
async delete(endpoint: string) {
return this.request(endpoint, { method: "DELETE" });
}
}
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{
name: "list_leads",
description: "List leads from Close CRM with optional search query",
inputSchema: {
type: "object" as const,
properties: {
query: { type: "string", description: "Search query (Close query syntax)" },
_limit: { type: "number", description: "Max results to return (default 100)" },
_skip: { type: "number", description: "Number of results to skip (pagination)" },
_fields: { type: "string", description: "Comma-separated list of fields to return" },
},
},
},
{
name: "get_lead",
description: "Get a specific lead by ID",
inputSchema: {
type: "object" as const,
properties: {
lead_id: { type: "string", description: "Lead ID (e.g., lead_xxx)" },
},
required: ["lead_id"],
},
},
{
name: "create_lead",
description: "Create a new lead in Close CRM",
inputSchema: {
type: "object" as const,
properties: {
name: { type: "string", description: "Lead/Company name" },
url: { type: "string", description: "Company website URL" },
description: { type: "string", description: "Lead description" },
status_id: { type: "string", description: "Lead status ID" },
contacts: {
type: "array",
description: "Array of contacts to add to the lead",
items: {
type: "object",
properties: {
name: { type: "string", description: "Contact name" },
title: { type: "string", description: "Job title" },
emails: {
type: "array",
items: {
type: "object",
properties: {
email: { type: "string" },
type: { type: "string", description: "office, home, direct, mobile, fax, other" },
},
},
},
phones: {
type: "array",
items: {
type: "object",
properties: {
phone: { type: "string" },
type: { type: "string", description: "office, home, direct, mobile, fax, other" },
},
},
},
},
},
},
addresses: {
type: "array",
description: "Lead addresses",
items: {
type: "object",
properties: {
address_1: { type: "string" },
address_2: { type: "string" },
city: { type: "string" },
state: { type: "string" },
zipcode: { type: "string" },
country: { type: "string" },
},
},
},
custom: { type: "object", description: "Custom field values (key-value pairs)" },
},
required: ["name"],
},
},
{
name: "update_lead",
description: "Update an existing lead",
inputSchema: {
type: "object" as const,
properties: {
lead_id: { type: "string", description: "Lead ID to update" },
name: { type: "string", description: "Lead/Company name" },
url: { type: "string", description: "Company website URL" },
description: { type: "string", description: "Lead description" },
status_id: { type: "string", description: "Lead status ID" },
custom: { type: "object", description: "Custom field values to update" },
},
required: ["lead_id"],
},
},
{
name: "list_opportunities",
description: "List opportunities from Close CRM",
inputSchema: {
type: "object" as const,
properties: {
lead_id: { type: "string", description: "Filter by lead ID" },
status_id: { type: "string", description: "Filter by opportunity status ID" },
user_id: { type: "string", description: "Filter by assigned user ID" },
_limit: { type: "number", description: "Max results to return" },
_skip: { type: "number", description: "Number of results to skip" },
},
},
},
{
name: "create_opportunity",
description: "Create a new opportunity/deal",
inputSchema: {
type: "object" as const,
properties: {
lead_id: { type: "string", description: "Lead ID to attach opportunity to" },
status_id: { type: "string", description: "Opportunity status ID" },
value: { type: "number", description: "Deal value in cents" },
value_period: { type: "string", description: "one_time, monthly, annual" },
confidence: { type: "number", description: "Confidence percentage (0-100)" },
note: { type: "string", description: "Opportunity notes" },
date_won: { type: "string", description: "Date won (YYYY-MM-DD)" },
},
required: ["lead_id"],
},
},
{
name: "create_activity",
description: "Create an activity (note, call, email, meeting, etc.)",
inputSchema: {
type: "object" as const,
properties: {
activity_type: { type: "string", description: "Type: Note, Call, Email, Meeting, SMS" },
lead_id: { type: "string", description: "Lead ID for the activity" },
contact_id: { type: "string", description: "Contact ID (optional)" },
user_id: { type: "string", description: "User ID who performed activity" },
note: { type: "string", description: "Activity note/body content" },
subject: { type: "string", description: "Subject (for emails)" },
status: { type: "string", description: "Call status: completed, no-answer, busy, etc." },
direction: { type: "string", description: "inbound or outbound" },
duration: { type: "number", description: "Duration in seconds (for calls)" },
date_created: { type: "string", description: "Activity date (ISO 8601)" },
},
required: ["activity_type", "lead_id"],
},
},
{
name: "list_tasks",
description: "List tasks from Close CRM",
inputSchema: {
type: "object" as const,
properties: {
lead_id: { type: "string", description: "Filter by lead ID" },
assigned_to: { type: "string", description: "Filter by assigned user ID" },
is_complete: { type: "boolean", description: "Filter by completion status" },
_type: { type: "string", description: "Task type: lead, opportunity, incoming_email, missed_call, etc." },
_limit: { type: "number", description: "Max results to return" },
_skip: { type: "number", description: "Number of results to skip" },
},
},
},
{
name: "create_task",
description: "Create a new task",
inputSchema: {
type: "object" as const,
properties: {
lead_id: { type: "string", description: "Lead ID for the task" },
assigned_to: { type: "string", description: "User ID to assign task to" },
text: { type: "string", description: "Task description" },
date: { type: "string", description: "Due date (YYYY-MM-DD or ISO 8601)" },
is_complete: { type: "boolean", description: "Task completion status" },
},
required: ["lead_id", "text"],
},
},
{
name: "send_email",
description: "Send an email through Close CRM",
inputSchema: {
type: "object" as const,
properties: {
lead_id: { type: "string", description: "Lead ID" },
contact_id: { type: "string", description: "Contact ID to send to" },
to: { type: "array", items: { type: "string" }, description: "Recipient email addresses" },
cc: { type: "array", items: { type: "string" }, description: "CC email addresses" },
bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" },
subject: { type: "string", description: "Email subject" },
body_text: { type: "string", description: "Plain text body" },
body_html: { type: "string", description: "HTML body" },
status: { type: "string", description: "draft, outbox, sent" },
template_id: { type: "string", description: "Email template ID to use" },
},
required: ["lead_id", "to", "subject"],
},
},
{
name: "list_statuses",
description: "List lead and opportunity statuses",
inputSchema: {
type: "object" as const,
properties: {
type: { type: "string", description: "lead or opportunity" },
},
},
},
{
name: "list_users",
description: "List users in the Close organization",
inputSchema: {
type: "object" as const,
properties: {},
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: CloseClient, name: string, args: any) {
switch (name) {
case "list_leads": {
const { query, _limit = 100, _skip, _fields } = args;
const params: any = { _limit };
if (query) params.query = query;
if (_skip) params._skip = _skip;
if (_fields) params._fields = _fields;
return await client.get("/lead/", params);
}
case "get_lead": {
const { lead_id } = args;
return await client.get(`/lead/${lead_id}/`);
}
case "create_lead": {
const { name, url, description, status_id, contacts, addresses, custom } = args;
const data: any = { name };
if (url) data.url = url;
if (description) data.description = description;
if (status_id) data.status_id = status_id;
if (contacts) data.contacts = contacts;
if (addresses) data.addresses = addresses;
if (custom) data.custom = custom;
return await client.post("/lead/", data);
}
case "update_lead": {
const { lead_id, ...updates } = args;
return await client.put(`/lead/${lead_id}/`, updates);
}
case "list_opportunities": {
const { lead_id, status_id, user_id, _limit = 100, _skip } = args;
const params: any = { _limit };
if (lead_id) params.lead_id = lead_id;
if (status_id) params.status_id = status_id;
if (user_id) params.user_id = user_id;
if (_skip) params._skip = _skip;
return await client.get("/opportunity/", params);
}
case "create_opportunity": {
const { lead_id, status_id, value, value_period, confidence, note, date_won } = args;
const data: any = { lead_id };
if (status_id) data.status_id = status_id;
if (value !== undefined) data.value = value;
if (value_period) data.value_period = value_period;
if (confidence !== undefined) data.confidence = confidence;
if (note) data.note = note;
if (date_won) data.date_won = date_won;
return await client.post("/opportunity/", data);
}
case "create_activity": {
const { activity_type, lead_id, contact_id, user_id, note, subject, status, direction, duration, date_created } = args;
// Map activity type to endpoint
const typeMap: Record<string, string> = {
'Note': 'note',
'Call': 'call',
'Email': 'email',
'Meeting': 'meeting',
'SMS': 'sms',
};
const endpoint = typeMap[activity_type] || activity_type.toLowerCase();
const data: any = { lead_id };
if (contact_id) data.contact_id = contact_id;
if (user_id) data.user_id = user_id;
if (note) data.note = note;
if (subject) data.subject = subject;
if (status) data.status = status;
if (direction) data.direction = direction;
if (duration) data.duration = duration;
if (date_created) data.date_created = date_created;
return await client.post(`/activity/${endpoint}/`, data);
}
case "list_tasks": {
const { lead_id, assigned_to, is_complete, _type, _limit = 100, _skip } = args;
const params: any = { _limit };
if (lead_id) params.lead_id = lead_id;
if (assigned_to) params.assigned_to = assigned_to;
if (is_complete !== undefined) params.is_complete = is_complete;
if (_type) params._type = _type;
if (_skip) params._skip = _skip;
return await client.get("/task/", params);
}
case "create_task": {
const { lead_id, assigned_to, text, date, is_complete } = args;
const data: any = { lead_id, text };
if (assigned_to) data.assigned_to = assigned_to;
if (date) data.date = date;
if (is_complete !== undefined) data.is_complete = is_complete;
return await client.post("/task/", data);
}
case "send_email": {
const { lead_id, contact_id, to, cc, bcc, subject, body_text, body_html, status, template_id } = args;
const data: any = { lead_id, to, subject };
if (contact_id) data.contact_id = contact_id;
if (cc) data.cc = cc;
if (bcc) data.bcc = bcc;
if (body_text) data.body_text = body_text;
if (body_html) data.body_html = body_html;
if (status) data.status = status;
if (template_id) data.template_id = template_id;
return await client.post("/activity/email/", data);
}
case "list_statuses": {
const { type } = args;
if (type === 'opportunity') {
return await client.get("/status/opportunity/");
}
return await client.get("/status/lead/");
}
case "list_users": {
return await client.get("/user/");
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const apiKey = process.env.CLOSE_API_KEY;
if (!apiKey) {
console.error("Error: CLOSE_API_KEY environment variable required");
console.error("Get your API key at Settings > Integrations > API Keys in Close");
process.exit(1);
}
const client = new CloseClient(apiKey);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);

View File

@ -0,0 +1,396 @@
// Activity management tools (notes, calls, emails, SMS, meetings)
import type { CloseClient } from "../client/close-client.js";
import type { Activity, Note, Call, Email, SMS, Meeting } from "../types/index.js";
export function registerActivitiesTools(server: any, client: CloseClient) {
// List activities
server.tool(
"close_list_activities",
"List activities (notes, calls, emails, etc.)",
{
lead_id: {
type: "string",
description: "Filter by lead ID",
required: false,
},
type: {
type: "string",
description: "Activity type (note, call, email, sms, meeting)",
required: false,
},
limit: {
type: "number",
description: "Number of results",
required: false,
},
},
async (args: any) => {
const endpoint = args.type ? `/activity/${args.type}/` : "/activity/";
const params: any = { limit: args.limit };
if (args.lead_id) params.lead_id = args.lead_id;
const activities = await client.search<Activity>(endpoint, params);
return {
content: [
{
type: "text",
text: JSON.stringify(activities, null, 2),
},
],
};
}
);
// Get activity
server.tool(
"close_get_activity",
"Get a specific activity by type and ID",
{
activity_type: {
type: "string",
description: "Activity type (note, call, email, sms, meeting)",
required: true,
},
activity_id: {
type: "string",
description: "Activity ID",
required: true,
},
},
async (args: any) => {
const activity = await client.get<Activity>(
`/activity/${args.activity_type}/${args.activity_id}/`
);
return {
content: [
{
type: "text",
text: JSON.stringify(activity, null, 2),
},
],
};
}
);
// Create note
server.tool(
"close_create_note",
"Create a note activity",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
note: {
type: "string",
description: "Note content",
required: true,
},
contact_id: {
type: "string",
description: "Contact ID (optional)",
required: false,
},
},
async (args: any) => {
const body: any = {
lead_id: args.lead_id,
note: args.note,
};
if (args.contact_id) {
body.contact_id = args.contact_id;
}
const note = await client.post<Note>("/activity/note/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(note, null, 2),
},
],
};
}
);
// Create call
server.tool(
"close_create_call",
"Log a call activity",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
direction: {
type: "string",
description: "Call direction (inbound or outbound)",
required: true,
},
phone: {
type: "string",
description: "Phone number",
required: false,
},
duration: {
type: "number",
description: "Call duration in seconds",
required: false,
},
note: {
type: "string",
description: "Call notes",
required: false,
},
disposition: {
type: "string",
description: "Call disposition",
required: false,
},
},
async (args: any) => {
const body: any = {
lead_id: args.lead_id,
direction: args.direction,
};
if (args.phone) body.phone = args.phone;
if (args.duration) body.duration = args.duration;
if (args.note) body.note = args.note;
if (args.disposition) body.disposition = args.disposition;
const call = await client.post<Call>("/activity/call/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(call, null, 2),
},
],
};
}
);
// Create email
server.tool(
"close_create_email",
"Log an email activity",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
direction: {
type: "string",
description: "Email direction (inbound or outbound)",
required: true,
},
subject: {
type: "string",
description: "Email subject",
required: true,
},
body_text: {
type: "string",
description: "Email body (plain text)",
required: false,
},
body_html: {
type: "string",
description: "Email body (HTML)",
required: false,
},
sender: {
type: "string",
description: "Sender email address",
required: false,
},
to: {
type: "string",
description: "JSON array of recipient email addresses",
required: false,
},
},
async (args: any) => {
const body: any = {
lead_id: args.lead_id,
direction: args.direction,
subject: args.subject,
};
if (args.body_text) body.body_text = args.body_text;
if (args.body_html) body.body_html = args.body_html;
if (args.sender) body.sender = args.sender;
if (args.to) body.to = JSON.parse(args.to);
const email = await client.post<Email>("/activity/email/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(email, null, 2),
},
],
};
}
);
// Create SMS
server.tool(
"close_create_sms",
"Log an SMS activity",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
text: {
type: "string",
description: "SMS message text",
required: true,
},
direction: {
type: "string",
description: "SMS direction (inbound or outbound)",
required: true,
},
remote_phone: {
type: "string",
description: "Remote phone number",
required: false,
},
local_phone: {
type: "string",
description: "Local phone number",
required: false,
},
},
async (args: any) => {
const body: any = {
lead_id: args.lead_id,
text: args.text,
direction: args.direction,
};
if (args.remote_phone) body.remote_phone = args.remote_phone;
if (args.local_phone) body.local_phone = args.local_phone;
const sms = await client.post<SMS>("/activity/sms/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(sms, null, 2),
},
],
};
}
);
// Create meeting
server.tool(
"close_create_meeting",
"Create a meeting activity",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
title: {
type: "string",
description: "Meeting title",
required: true,
},
starts_at: {
type: "string",
description: "Start time (ISO 8601 format)",
required: true,
},
ends_at: {
type: "string",
description: "End time (ISO 8601 format)",
required: false,
},
location: {
type: "string",
description: "Meeting location",
required: false,
},
note: {
type: "string",
description: "Meeting notes",
required: false,
},
},
async (args: any) => {
const body: any = {
lead_id: args.lead_id,
title: args.title,
starts_at: args.starts_at,
};
if (args.ends_at) body.ends_at = args.ends_at;
if (args.location) body.location = args.location;
if (args.note) body.note = args.note;
const meeting = await client.post<Meeting>("/activity/meeting/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(meeting, null, 2),
},
],
};
}
);
// Log generic activity
server.tool(
"close_log_activity",
"Log a generic activity",
{
activity_type: {
type: "string",
description: "Activity type",
required: true,
},
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
data: {
type: "string",
description: "JSON object with activity data",
required: true,
},
},
async (args: any) => {
const body = {
lead_id: args.lead_id,
...JSON.parse(args.data),
};
const activity = await client.post<Activity>(
`/activity/${args.activity_type}/`,
body
);
return {
content: [
{
type: "text",
text: JSON.stringify(activity, null, 2),
},
],
};
}
);
}

View File

@ -0,0 +1,204 @@
// Bulk operations tools
import type { CloseClient } from "../client/close-client.js";
import type {
BulkEditRequest,
BulkDeleteRequest,
BulkEmailRequest,
} from "../types/index.js";
export function registerBulkTools(server: any, client: CloseClient) {
// Bulk edit leads
server.tool(
"close_bulk_edit_leads",
"Bulk edit multiple leads at once",
{
lead_ids: {
type: "string",
description: "JSON array of lead IDs",
required: false,
},
query: {
type: "string",
description: "Search query to select leads",
required: false,
},
updates: {
type: "string",
description: "JSON object with field updates",
required: true,
},
},
async (args: any) => {
const body: BulkEditRequest = {
updates: JSON.parse(args.updates),
};
if (args.lead_ids) {
body.lead_ids = JSON.parse(args.lead_ids);
}
if (args.query) {
body.query = args.query;
}
const result = await client.post("/lead/bulk_action/edit/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
// Bulk delete leads
server.tool(
"close_bulk_delete_leads",
"Bulk delete multiple leads",
{
lead_ids: {
type: "string",
description: "JSON array of lead IDs",
required: false,
},
query: {
type: "string",
description: "Search query to select leads",
required: false,
},
},
async (args: any) => {
const body: BulkDeleteRequest = {};
if (args.lead_ids) {
body.lead_ids = JSON.parse(args.lead_ids);
}
if (args.query) {
body.query = args.query;
}
const result = await client.post("/lead/bulk_action/delete/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
// Bulk email
server.tool(
"close_bulk_email",
"Send bulk email to multiple leads",
{
lead_ids: {
type: "string",
description: "JSON array of lead IDs",
required: false,
},
query: {
type: "string",
description: "Search query to select leads",
required: false,
},
subject: {
type: "string",
description: "Email subject",
required: true,
},
body: {
type: "string",
description: "Email body (HTML or plain text)",
required: true,
},
template_id: {
type: "string",
description: "Email template ID",
required: false,
},
sender: {
type: "string",
description: "Sender email address",
required: false,
},
},
async (args: any) => {
const body: BulkEmailRequest = {
subject: args.subject,
body: args.body,
};
if (args.lead_ids) {
body.lead_ids = JSON.parse(args.lead_ids);
}
if (args.query) {
body.query = args.query;
}
if (args.template_id) {
body.template_id = args.template_id;
}
if (args.sender) {
body.sender = args.sender;
}
const result = await client.post("/bulk_action/email/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
// Bulk update opportunity status
server.tool(
"close_bulk_update_opportunity_status",
"Bulk update opportunity statuses",
{
opportunity_ids: {
type: "string",
description: "JSON array of opportunity IDs",
required: true,
},
status_id: {
type: "string",
description: "New status ID",
required: true,
},
},
async (args: any) => {
const body = {
opportunity_ids: JSON.parse(args.opportunity_ids),
updates: {
status_id: args.status_id,
},
};
const result = await client.post(
"/opportunity/bulk_action/edit/",
body
);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
}

View File

@ -0,0 +1,211 @@
// Contact management tools
import type { CloseClient } from "../client/close-client.js";
import type { Contact } from "../types/index.js";
export function registerContactsTools(server: any, client: CloseClient) {
// List contacts
server.tool(
"close_list_contacts",
"List all contacts with optional filtering",
{
lead_id: {
type: "string",
description: "Filter by lead ID",
required: false,
},
query: {
type: "string",
description: "Search query",
required: false,
},
limit: {
type: "number",
description: "Number of results",
required: false,
},
},
async (args: any) => {
const params: any = {
query: args.query,
limit: args.limit,
};
if (args.lead_id) {
params.lead_id = args.lead_id;
}
const contacts = await client.search<Contact>("/contact/", params);
return {
content: [
{
type: "text",
text: JSON.stringify(contacts, null, 2),
},
],
};
}
);
// Get contact
server.tool(
"close_get_contact",
"Get a specific contact by ID",
{
contact_id: {
type: "string",
description: "Contact ID",
required: true,
},
},
async (args: any) => {
const contact = await client.get<Contact>(
`/contact/${args.contact_id}/`
);
return {
content: [
{
type: "text",
text: JSON.stringify(contact, null, 2),
},
],
};
}
);
// Create contact
server.tool(
"close_create_contact",
"Create a new contact",
{
lead_id: {
type: "string",
description: "Lead ID to associate contact with",
required: true,
},
name: {
type: "string",
description: "Contact name",
required: true,
},
title: {
type: "string",
description: "Job title",
required: false,
},
emails: {
type: "string",
description: 'JSON array of email objects (e.g., [{"email":"test@example.com","type":"office"}])',
required: false,
},
phones: {
type: "string",
description: 'JSON array of phone objects (e.g., [{"phone":"+1234567890","type":"mobile"}])',
required: false,
},
},
async (args: any) => {
const body: any = {
lead_id: args.lead_id,
name: args.name,
title: args.title,
};
if (args.emails) {
body.emails = JSON.parse(args.emails);
}
if (args.phones) {
body.phones = JSON.parse(args.phones);
}
const contact = await client.post<Contact>("/contact/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(contact, null, 2),
},
],
};
}
);
// Update contact
server.tool(
"close_update_contact",
"Update an existing contact",
{
contact_id: {
type: "string",
description: "Contact ID",
required: true,
},
name: {
type: "string",
description: "Contact name",
required: false,
},
title: {
type: "string",
description: "Job title",
required: false,
},
emails: {
type: "string",
description: "JSON array of email objects",
required: false,
},
phones: {
type: "string",
description: "JSON array of phone objects",
required: false,
},
},
async (args: any) => {
const body: any = {};
if (args.name) body.name = args.name;
if (args.title) body.title = args.title;
if (args.emails) body.emails = JSON.parse(args.emails);
if (args.phones) body.phones = JSON.parse(args.phones);
const contact = await client.put<Contact>(
`/contact/${args.contact_id}/`,
body
);
return {
content: [
{
type: "text",
text: JSON.stringify(contact, null, 2),
},
],
};
}
);
// Delete contact
server.tool(
"close_delete_contact",
"Delete a contact",
{
contact_id: {
type: "string",
description: "Contact ID",
required: true,
},
},
async (args: any) => {
await client.delete(`/contact/${args.contact_id}/`);
return {
content: [
{
type: "text",
text: `Contact ${args.contact_id} deleted successfully`,
},
],
};
}
);
}

View File

@ -0,0 +1,212 @@
// Custom fields management tools
import type { CloseClient } from "../client/close-client.js";
import type { CustomField } from "../types/index.js";
export function registerCustomFieldsTools(server: any, client: CloseClient) {
// List custom fields
server.tool(
"close_list_custom_fields",
"List all custom fields",
{
object_type: {
type: "string",
description: "Object type (lead, contact, opportunity, activity)",
required: false,
},
},
async (args: any) => {
const endpoint = args.object_type
? `/custom_field/${args.object_type}/`
: "/custom_field/";
const fields = await client.get<{ data: CustomField[] }>(endpoint);
return {
content: [
{
type: "text",
text: JSON.stringify(fields, null, 2),
},
],
};
}
);
// Get custom field
server.tool(
"close_get_custom_field",
"Get a specific custom field by ID",
{
object_type: {
type: "string",
description: "Object type (lead, contact, opportunity, activity)",
required: true,
},
field_id: {
type: "string",
description: "Custom field ID",
required: true,
},
},
async (args: any) => {
const field = await client.get<CustomField>(
`/custom_field/${args.object_type}/${args.field_id}/`
);
return {
content: [
{
type: "text",
text: JSON.stringify(field, null, 2),
},
],
};
}
);
// Create custom field
server.tool(
"close_create_custom_field",
"Create a new custom field",
{
object_type: {
type: "string",
description: "Object type (lead, contact, opportunity, activity)",
required: true,
},
name: {
type: "string",
description: "Field name",
required: true,
},
type: {
type: "string",
description:
"Field type (text, number, date, datetime, boolean, choices)",
required: true,
},
required: {
type: "boolean",
description: "Is field required?",
required: false,
},
accepts_multiple_values: {
type: "boolean",
description: "Accept multiple values?",
required: false,
},
choices: {
type: "string",
description: "JSON array of choice values (for choices type)",
required: false,
},
},
async (args: any) => {
const body: any = {
name: args.name,
type: args.type,
};
if (args.required !== undefined) body.required = args.required;
if (args.accepts_multiple_values !== undefined)
body.accepts_multiple_values = args.accepts_multiple_values;
if (args.choices) body.choices = JSON.parse(args.choices);
const field = await client.post<CustomField>(
`/custom_field/${args.object_type}/`,
body
);
return {
content: [
{
type: "text",
text: JSON.stringify(field, null, 2),
},
],
};
}
);
// Update custom field
server.tool(
"close_update_custom_field",
"Update an existing custom field",
{
object_type: {
type: "string",
description: "Object type",
required: true,
},
field_id: {
type: "string",
description: "Custom field ID",
required: true,
},
name: {
type: "string",
description: "New field name",
required: false,
},
required: {
type: "boolean",
description: "Is field required?",
required: false,
},
choices: {
type: "string",
description: "JSON array of choice values",
required: false,
},
},
async (args: any) => {
const body: any = {};
if (args.name) body.name = args.name;
if (args.required !== undefined) body.required = args.required;
if (args.choices) body.choices = JSON.parse(args.choices);
const field = await client.put<CustomField>(
`/custom_field/${args.object_type}/${args.field_id}/`,
body
);
return {
content: [
{
type: "text",
text: JSON.stringify(field, null, 2),
},
],
};
}
);
// Delete custom field
server.tool(
"close_delete_custom_field",
"Delete a custom field",
{
object_type: {
type: "string",
description: "Object type",
required: true,
},
field_id: {
type: "string",
description: "Custom field ID",
required: true,
},
},
async (args: any) => {
await client.delete(
`/custom_field/${args.object_type}/${args.field_id}/`
);
return {
content: [
{
type: "text",
text: `Custom field ${args.field_id} deleted successfully`,
},
],
};
}
);
}

View File

@ -0,0 +1,301 @@
// Lead management tools
import type { CloseClient } from "../client/close-client.js";
import type { Lead, CustomField } from "../types/index.js";
export function registerLeadsTools(server: any, client: CloseClient) {
// List leads
server.tool(
"close_list_leads",
"List all leads with optional filtering",
{
query: {
type: "string",
description: "Search query (e.g., 'name:Acme')",
required: false,
},
limit: {
type: "number",
description: "Number of results to return",
required: false,
},
},
async (args: any) => {
const leads = await client.search<Lead>("/lead/", {
query: args.query,
limit: args.limit,
});
return {
content: [
{
type: "text",
text: JSON.stringify(leads, null, 2),
},
],
};
}
);
// Get lead by ID
server.tool(
"close_get_lead",
"Get a specific lead by ID",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
},
async (args: any) => {
const lead = await client.get<Lead>(`/lead/${args.lead_id}/`);
return {
content: [
{
type: "text",
text: JSON.stringify(lead, null, 2),
},
],
};
}
);
// Create lead
server.tool(
"close_create_lead",
"Create a new lead",
{
name: {
type: "string",
description: "Lead name (company name)",
required: true,
},
description: {
type: "string",
description: "Lead description",
required: false,
},
url: {
type: "string",
description: "Lead website URL",
required: false,
},
status_id: {
type: "string",
description: "Lead status ID",
required: false,
},
contacts: {
type: "string",
description: "JSON array of contact objects",
required: false,
},
custom: {
type: "string",
description: "JSON object of custom field values",
required: false,
},
},
async (args: any) => {
const body: any = {
name: args.name,
description: args.description,
url: args.url,
status_id: args.status_id,
};
if (args.contacts) {
body.contacts = JSON.parse(args.contacts);
}
if (args.custom) {
body.custom = JSON.parse(args.custom);
}
const lead = await client.post<Lead>("/lead/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(lead, null, 2),
},
],
};
}
);
// Update lead
server.tool(
"close_update_lead",
"Update an existing lead",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
name: {
type: "string",
description: "Lead name",
required: false,
},
description: {
type: "string",
description: "Lead description",
required: false,
},
url: {
type: "string",
description: "Lead website URL",
required: false,
},
status_id: {
type: "string",
description: "Lead status ID",
required: false,
},
custom: {
type: "string",
description: "JSON object of custom field values",
required: false,
},
},
async (args: any) => {
const body: any = {};
if (args.name) body.name = args.name;
if (args.description) body.description = args.description;
if (args.url) body.url = args.url;
if (args.status_id) body.status_id = args.status_id;
if (args.custom) body.custom = JSON.parse(args.custom);
const lead = await client.put<Lead>(`/lead/${args.lead_id}/`, body);
return {
content: [
{
type: "text",
text: JSON.stringify(lead, null, 2),
},
],
};
}
);
// Delete lead
server.tool(
"close_delete_lead",
"Delete a lead",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
},
async (args: any) => {
await client.delete(`/lead/${args.lead_id}/`);
return {
content: [
{
type: "text",
text: `Lead ${args.lead_id} deleted successfully`,
},
],
};
}
);
// Search leads
server.tool(
"close_search_leads",
"Search leads with advanced query syntax",
{
query: {
type: "string",
description:
"Search query (e.g., 'name:Acme status:\"Potential\" email:*@acme.com')",
required: true,
},
limit: {
type: "number",
description: "Max results to return",
required: false,
},
},
async (args: any) => {
const leads = await client.search<Lead>("/lead/", {
query: args.query,
limit: args.limit || 100,
});
return {
content: [
{
type: "text",
text: JSON.stringify(
{
count: leads.length,
leads: leads,
},
null,
2
),
},
],
};
}
);
// Merge leads
server.tool(
"close_merge_leads",
"Merge two leads together",
{
source_lead_id: {
type: "string",
description: "Lead ID to merge from (will be deleted)",
required: true,
},
destination_lead_id: {
type: "string",
description: "Lead ID to merge into (will be kept)",
required: true,
},
},
async (args: any) => {
const result = await client.post(
`/lead/${args.destination_lead_id}/merge/`,
{
source: args.source_lead_id,
}
);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
// List lead custom fields
server.tool(
"close_list_lead_custom_fields",
"List all custom fields for leads",
{},
async () => {
const fields = await client.get<{ data: CustomField[] }>(
"/custom_field/lead/"
);
return {
content: [
{
type: "text",
text: JSON.stringify(fields, null, 2),
},
],
};
}
);
}

View File

@ -0,0 +1,247 @@
// Opportunity management tools
import type { CloseClient } from "../client/close-client.js";
import type { Opportunity, OpportunityStatus, Pipeline } from "../types/index.js";
export function registerOpportunitiesTools(server: any, client: CloseClient) {
// List opportunities
server.tool(
"close_list_opportunities",
"List all opportunities with optional filtering",
{
lead_id: {
type: "string",
description: "Filter by lead ID",
required: false,
},
status_id: {
type: "string",
description: "Filter by status ID",
required: false,
},
limit: {
type: "number",
description: "Number of results",
required: false,
},
},
async (args: any) => {
const params: any = { limit: args.limit };
if (args.lead_id) params.lead_id = args.lead_id;
if (args.status_id) params.status_id = args.status_id;
const opps = await client.search<Opportunity>("/opportunity/", params);
return {
content: [
{
type: "text",
text: JSON.stringify(opps, null, 2),
},
],
};
}
);
// Get opportunity
server.tool(
"close_get_opportunity",
"Get a specific opportunity by ID",
{
opportunity_id: {
type: "string",
description: "Opportunity ID",
required: true,
},
},
async (args: any) => {
const opp = await client.get<Opportunity>(
`/opportunity/${args.opportunity_id}/`
);
return {
content: [
{
type: "text",
text: JSON.stringify(opp, null, 2),
},
],
};
}
);
// Create opportunity
server.tool(
"close_create_opportunity",
"Create a new opportunity",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
status_id: {
type: "string",
description: "Opportunity status ID",
required: true,
},
value: {
type: "number",
description: "Opportunity value (amount)",
required: false,
},
value_period: {
type: "string",
description: "Value period (one_time, monthly, annual)",
required: false,
},
confidence: {
type: "number",
description: "Confidence percentage (0-100)",
required: false,
},
note: {
type: "string",
description: "Opportunity notes",
required: false,
},
},
async (args: any) => {
const body: any = {
lead_id: args.lead_id,
status_id: args.status_id,
};
if (args.value !== undefined) body.value = args.value;
if (args.value_period) body.value_period = args.value_period;
if (args.confidence !== undefined) body.confidence = args.confidence;
if (args.note) body.note = args.note;
const opp = await client.post<Opportunity>("/opportunity/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(opp, null, 2),
},
],
};
}
);
// Update opportunity
server.tool(
"close_update_opportunity",
"Update an existing opportunity",
{
opportunity_id: {
type: "string",
description: "Opportunity ID",
required: true,
},
status_id: {
type: "string",
description: "New status ID",
required: false,
},
value: {
type: "number",
description: "Opportunity value",
required: false,
},
confidence: {
type: "number",
description: "Confidence percentage",
required: false,
},
note: {
type: "string",
description: "Opportunity notes",
required: false,
},
},
async (args: any) => {
const body: any = {};
if (args.status_id) body.status_id = args.status_id;
if (args.value !== undefined) body.value = args.value;
if (args.confidence !== undefined) body.confidence = args.confidence;
if (args.note) body.note = args.note;
const opp = await client.put<Opportunity>(
`/opportunity/${args.opportunity_id}/`,
body
);
return {
content: [
{
type: "text",
text: JSON.stringify(opp, null, 2),
},
],
};
}
);
// Delete opportunity
server.tool(
"close_delete_opportunity",
"Delete an opportunity",
{
opportunity_id: {
type: "string",
description: "Opportunity ID",
required: true,
},
},
async (args: any) => {
await client.delete(`/opportunity/${args.opportunity_id}/`);
return {
content: [
{
type: "text",
text: `Opportunity ${args.opportunity_id} deleted successfully`,
},
],
};
}
);
// List opportunity statuses
server.tool(
"close_list_opportunity_statuses",
"List all opportunity statuses",
{},
async () => {
const statuses = await client.get<{ data: OpportunityStatus[] }>(
"/status/opportunity/"
);
return {
content: [
{
type: "text",
text: JSON.stringify(statuses, null, 2),
},
],
};
}
);
// List pipelines
server.tool(
"close_list_pipelines",
"List all opportunity pipelines",
{},
async () => {
const pipelines = await client.get<{ data: Pipeline[] }>(
"/pipeline/"
);
return {
content: [
{
type: "text",
text: JSON.stringify(pipelines, null, 2),
},
],
};
}
);
}

View File

@ -0,0 +1,166 @@
// Pipeline and status management tools
import type { CloseClient } from "../client/close-client.js";
import type { Pipeline, OpportunityStatus } from "../types/index.js";
export function registerPipelinesTools(server: any, client: CloseClient) {
// List pipelines
server.tool(
"close_list_pipelines",
"List all opportunity pipelines",
{},
async () => {
const pipelines = await client.get<{ data: Pipeline[] }>("/pipeline/");
return {
content: [
{
type: "text",
text: JSON.stringify(pipelines, null, 2),
},
],
};
}
);
// Get pipeline
server.tool(
"close_get_pipeline",
"Get a specific pipeline by ID",
{
pipeline_id: {
type: "string",
description: "Pipeline ID",
required: true,
},
},
async (args: any) => {
const pipeline = await client.get<Pipeline>(
`/pipeline/${args.pipeline_id}/`
);
return {
content: [
{
type: "text",
text: JSON.stringify(pipeline, null, 2),
},
],
};
}
);
// Create pipeline
server.tool(
"close_create_pipeline",
"Create a new opportunity pipeline",
{
name: {
type: "string",
description: "Pipeline name",
required: true,
},
},
async (args: any) => {
const pipeline = await client.post<Pipeline>("/pipeline/", {
name: args.name,
});
return {
content: [
{
type: "text",
text: JSON.stringify(pipeline, null, 2),
},
],
};
}
);
// Update pipeline
server.tool(
"close_update_pipeline",
"Update an existing pipeline",
{
pipeline_id: {
type: "string",
description: "Pipeline ID",
required: true,
},
name: {
type: "string",
description: "New pipeline name",
required: true,
},
},
async (args: any) => {
const pipeline = await client.put<Pipeline>(
`/pipeline/${args.pipeline_id}/`,
{
name: args.name,
}
);
return {
content: [
{
type: "text",
text: JSON.stringify(pipeline, null, 2),
},
],
};
}
);
// Delete pipeline
server.tool(
"close_delete_pipeline",
"Delete a pipeline",
{
pipeline_id: {
type: "string",
description: "Pipeline ID",
required: true,
},
},
async (args: any) => {
await client.delete(`/pipeline/${args.pipeline_id}/`);
return {
content: [
{
type: "text",
text: `Pipeline ${args.pipeline_id} deleted successfully`,
},
],
};
}
);
// List opportunity statuses
server.tool(
"close_list_opportunity_statuses",
"List all opportunity statuses",
{
pipeline_id: {
type: "string",
description: "Filter by pipeline ID",
required: false,
},
},
async (args: any) => {
const endpoint = "/status/opportunity/";
const params = args.pipeline_id
? { pipeline_id: args.pipeline_id }
: undefined;
const statuses = await client.get<{ data: OpportunityStatus[] }>(
endpoint,
params
);
return {
content: [
{
type: "text",
text: JSON.stringify(statuses, null, 2),
},
],
};
}
);
}

View File

@ -0,0 +1,248 @@
// Reporting and analytics tools
import type { CloseClient } from "../client/close-client.js";
export function registerReportingTools(server: any, client: CloseClient) {
// Lead status changes report
server.tool(
"close_report_lead_status_changes",
"Get report of lead status changes over time",
{
date_start: {
type: "string",
description: "Start date (YYYY-MM-DD)",
required: false,
},
date_end: {
type: "string",
description: "End date (YYYY-MM-DD)",
required: false,
},
status_id: {
type: "string",
description: "Filter by status ID",
required: false,
},
},
async (args: any) => {
const params: any = {};
if (args.date_start) params.date_start = args.date_start;
if (args.date_end) params.date_end = args.date_end;
if (args.status_id) params.status_id = args.status_id;
const report = await client.get("/report/lead_status_changes/", params);
return {
content: [
{
type: "text",
text: JSON.stringify(report, null, 2),
},
],
};
}
);
// Opportunity funnel report
server.tool(
"close_report_opportunity_funnel",
"Get opportunity funnel/pipeline analytics",
{
date_start: {
type: "string",
description: "Start date (YYYY-MM-DD)",
required: false,
},
date_end: {
type: "string",
description: "End date (YYYY-MM-DD)",
required: false,
},
pipeline_id: {
type: "string",
description: "Filter by pipeline ID",
required: false,
},
user_id: {
type: "string",
description: "Filter by user ID",
required: false,
},
},
async (args: any) => {
const params: any = {};
if (args.date_start) params.date_start = args.date_start;
if (args.date_end) params.date_end = args.date_end;
if (args.pipeline_id) params.pipeline_id = args.pipeline_id;
if (args.user_id) params.user_id = args.user_id;
const report = await client.get("/report/opportunity/", params);
return {
content: [
{
type: "text",
text: JSON.stringify(report, null, 2),
},
],
};
}
);
// Activity overview report
server.tool(
"close_report_activity_overview",
"Get activity summary and metrics",
{
date_start: {
type: "string",
description: "Start date (YYYY-MM-DD)",
required: false,
},
date_end: {
type: "string",
description: "End date (YYYY-MM-DD)",
required: false,
},
user_id: {
type: "string",
description: "Filter by user ID",
required: false,
},
activity_type: {
type: "string",
description: "Filter by activity type",
required: false,
},
},
async (args: any) => {
const params: any = {};
if (args.date_start) params.date_start = args.date_start;
if (args.date_end) params.date_end = args.date_end;
if (args.user_id) params.user_id = args.user_id;
if (args.activity_type) params.activity_type = args.activity_type;
const report = await client.get("/report/activity/", params);
return {
content: [
{
type: "text",
text: JSON.stringify(report, null, 2),
},
],
};
}
);
// Revenue forecast report
server.tool(
"close_report_revenue_forecast",
"Get revenue forecast based on opportunities",
{
date_start: {
type: "string",
description: "Start date (YYYY-MM-DD)",
required: false,
},
date_end: {
type: "string",
description: "End date (YYYY-MM-DD)",
required: false,
},
pipeline_id: {
type: "string",
description: "Filter by pipeline ID",
required: false,
},
user_id: {
type: "string",
description: "Filter by user ID",
required: false,
},
},
async (args: any) => {
const params: any = {};
if (args.date_start) params.date_start = args.date_start;
if (args.date_end) params.date_end = args.date_end;
if (args.pipeline_id) params.pipeline_id = args.pipeline_id;
if (args.user_id) params.user_id = args.user_id;
const report = await client.get("/report/revenue/", params);
return {
content: [
{
type: "text",
text: JSON.stringify(report, null, 2),
},
],
};
}
);
// Leaderboard report
server.tool(
"close_report_leaderboard",
"Get user performance leaderboard",
{
date_start: {
type: "string",
description: "Start date (YYYY-MM-DD)",
required: false,
},
date_end: {
type: "string",
description: "End date (YYYY-MM-DD)",
required: false,
},
metric: {
type: "string",
description: "Metric to rank by (calls, emails, opportunities_won)",
required: false,
},
},
async (args: any) => {
const params: any = {};
if (args.date_start) params.date_start = args.date_start;
if (args.date_end) params.date_end = args.date_end;
if (args.metric) params.metric = args.metric;
const report = await client.get("/report/leaderboard/", params);
return {
content: [
{
type: "text",
text: JSON.stringify(report, null, 2),
},
],
};
}
);
// Custom report query
server.tool(
"close_report_custom",
"Run a custom report query",
{
report_type: {
type: "string",
description: "Report type/endpoint",
required: true,
},
params: {
type: "string",
description: "JSON object with report parameters",
required: false,
},
},
async (args: any) => {
const params = args.params ? JSON.parse(args.params) : {};
const report = await client.get(`/report/${args.report_type}/`, params);
return {
content: [
{
type: "text",
text: JSON.stringify(report, null, 2),
},
],
};
}
);
}

View File

@ -0,0 +1,188 @@
// Sequences and automation tools
import type { CloseClient } from "../client/close-client.js";
import type { Sequence, SequenceSubscription } from "../types/index.js";
export function registerSequencesTools(server: any, client: CloseClient) {
// List sequences
server.tool(
"close_list_sequences",
"List all email sequences",
{},
async () => {
const sequences = await client.get<{ data: Sequence[] }>("/sequence/");
return {
content: [
{
type: "text",
text: JSON.stringify(sequences, null, 2),
},
],
};
}
);
// Get sequence
server.tool(
"close_get_sequence",
"Get a specific sequence by ID",
{
sequence_id: {
type: "string",
description: "Sequence ID",
required: true,
},
},
async (args: any) => {
const sequence = await client.get<Sequence>(
`/sequence/${args.sequence_id}/`
);
return {
content: [
{
type: "text",
text: JSON.stringify(sequence, null, 2),
},
],
};
}
);
// Create sequence
server.tool(
"close_create_sequence",
"Create a new email sequence",
{
name: {
type: "string",
description: "Sequence name",
required: true,
},
max_activations: {
type: "number",
description: "Maximum activations per lead",
required: false,
},
},
async (args: any) => {
const body: any = {
name: args.name,
};
if (args.max_activations)
body.max_activations = args.max_activations;
const sequence = await client.post<Sequence>("/sequence/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(sequence, null, 2),
},
],
};
}
);
// Subscribe lead to sequence
server.tool(
"close_subscribe_lead_to_sequence",
"Subscribe a lead to an email sequence",
{
sequence_id: {
type: "string",
description: "Sequence ID",
required: true,
},
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
contact_id: {
type: "string",
description: "Contact ID (optional, for specific contact)",
required: false,
},
sender_account_id: {
type: "string",
description: "Sender email account ID",
required: false,
},
},
async (args: any) => {
const body: any = {
lead_id: args.lead_id,
};
if (args.contact_id) body.contact_id = args.contact_id;
if (args.sender_account_id)
body.sender_account_id = args.sender_account_id;
const subscription = await client.post<SequenceSubscription>(
`/sequence/${args.sequence_id}/subscribe/`,
body
);
return {
content: [
{
type: "text",
text: JSON.stringify(subscription, null, 2),
},
],
};
}
);
// Unsubscribe lead from sequence
server.tool(
"close_unsubscribe_lead_from_sequence",
"Unsubscribe a lead from a sequence",
{
subscription_id: {
type: "string",
description: "Sequence subscription ID",
required: true,
},
},
async (args: any) => {
await client.delete(
`/sequence_subscription/${args.subscription_id}/`
);
return {
content: [
{
type: "text",
text: `Lead unsubscribed from sequence successfully`,
},
],
};
}
);
// Get sequence stats
server.tool(
"close_get_sequence_stats",
"Get statistics for a sequence",
{
sequence_id: {
type: "string",
description: "Sequence ID",
required: true,
},
},
async (args: any) => {
const stats = await client.get(
`/sequence/${args.sequence_id}/stats/`
);
return {
content: [
{
type: "text",
text: JSON.stringify(stats, null, 2),
},
],
};
}
);
}

View File

@ -0,0 +1,159 @@
// Smart Views management tools
import type { CloseClient } from "../client/close-client.js";
import type { SmartView } from "../types/index.js";
export function registerSmartViewsTools(server: any, client: CloseClient) {
// List smart views
server.tool(
"close_list_smart_views",
"List all smart views",
{},
async () => {
const views = await client.get<{ data: SmartView[] }>("/saved_search/");
return {
content: [
{
type: "text",
text: JSON.stringify(views, null, 2),
},
],
};
}
);
// Get smart view
server.tool(
"close_get_smart_view",
"Get a specific smart view by ID",
{
view_id: {
type: "string",
description: "Smart view ID",
required: true,
},
},
async (args: any) => {
const view = await client.get<SmartView>(
`/saved_search/${args.view_id}/`
);
return {
content: [
{
type: "text",
text: JSON.stringify(view, null, 2),
},
],
};
}
);
// Create smart view
server.tool(
"close_create_smart_view",
"Create a new smart view (saved search)",
{
name: {
type: "string",
description: "Smart view name",
required: true,
},
query: {
type: "string",
description: "Search query",
required: true,
},
type: {
type: "string",
description: "View type (e.g., 'lead')",
required: false,
},
},
async (args: any) => {
const body: any = {
name: args.name,
query: args.query,
};
if (args.type) {
body.type = args.type;
}
const view = await client.post<SmartView>("/saved_search/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(view, null, 2),
},
],
};
}
);
// Update smart view
server.tool(
"close_update_smart_view",
"Update an existing smart view",
{
view_id: {
type: "string",
description: "Smart view ID",
required: true,
},
name: {
type: "string",
description: "New name",
required: false,
},
query: {
type: "string",
description: "New query",
required: false,
},
},
async (args: any) => {
const body: any = {};
if (args.name) body.name = args.name;
if (args.query) body.query = args.query;
const view = await client.put<SmartView>(
`/saved_search/${args.view_id}/`,
body
);
return {
content: [
{
type: "text",
text: JSON.stringify(view, null, 2),
},
],
};
}
);
// Delete smart view
server.tool(
"close_delete_smart_view",
"Delete a smart view",
{
view_id: {
type: "string",
description: "Smart view ID",
required: true,
},
},
async (args: any) => {
await client.delete(`/saved_search/${args.view_id}/`);
return {
content: [
{
type: "text",
text: `Smart view ${args.view_id} deleted successfully`,
},
],
};
}
);
}

View File

@ -0,0 +1,230 @@
// Task management tools
import type { CloseClient } from "../client/close-client.js";
import type { Task } from "../types/index.js";
export function registerTasksTools(server: any, client: CloseClient) {
// List tasks
server.tool(
"close_list_tasks",
"List all tasks with optional filtering",
{
lead_id: {
type: "string",
description: "Filter by lead ID",
required: false,
},
assigned_to: {
type: "string",
description: "Filter by assigned user ID",
required: false,
},
is_complete: {
type: "boolean",
description: "Filter by completion status",
required: false,
},
limit: {
type: "number",
description: "Number of results",
required: false,
},
},
async (args: any) => {
const params: any = { limit: args.limit };
if (args.lead_id) params.lead_id = args.lead_id;
if (args.assigned_to) params.assigned_to = args.assigned_to;
if (args.is_complete !== undefined)
params.is_complete = args.is_complete;
const tasks = await client.search<Task>("/task/", params);
return {
content: [
{
type: "text",
text: JSON.stringify(tasks, null, 2),
},
],
};
}
);
// Get task
server.tool(
"close_get_task",
"Get a specific task by ID",
{
task_id: {
type: "string",
description: "Task ID",
required: true,
},
},
async (args: any) => {
const task = await client.get<Task>(`/task/${args.task_id}/`);
return {
content: [
{
type: "text",
text: JSON.stringify(task, null, 2),
},
],
};
}
);
// Create task
server.tool(
"close_create_task",
"Create a new task",
{
lead_id: {
type: "string",
description: "Lead ID",
required: true,
},
text: {
type: "string",
description: "Task description",
required: true,
},
assigned_to: {
type: "string",
description: "User ID to assign task to",
required: false,
},
date: {
type: "string",
description: "Due date (YYYY-MM-DD format)",
required: false,
},
type: {
type: "string",
description: "Task type (e.g., 'lead', 'inbox')",
required: false,
},
},
async (args: any) => {
const body: any = {
lead_id: args.lead_id,
text: args.text,
_type: args.type || "lead",
};
if (args.assigned_to) body.assigned_to = args.assigned_to;
if (args.date) body.date = args.date;
const task = await client.post<Task>("/task/", body);
return {
content: [
{
type: "text",
text: JSON.stringify(task, null, 2),
},
],
};
}
);
// Update task
server.tool(
"close_update_task",
"Update an existing task",
{
task_id: {
type: "string",
description: "Task ID",
required: true,
},
text: {
type: "string",
description: "Task description",
required: false,
},
assigned_to: {
type: "string",
description: "User ID to assign task to",
required: false,
},
date: {
type: "string",
description: "Due date",
required: false,
},
is_complete: {
type: "boolean",
description: "Completion status",
required: false,
},
},
async (args: any) => {
const body: any = {};
if (args.text) body.text = args.text;
if (args.assigned_to) body.assigned_to = args.assigned_to;
if (args.date) body.date = args.date;
if (args.is_complete !== undefined)
body.is_complete = args.is_complete;
const task = await client.put<Task>(`/task/${args.task_id}/`, body);
return {
content: [
{
type: "text",
text: JSON.stringify(task, null, 2),
},
],
};
}
);
// Delete task
server.tool(
"close_delete_task",
"Delete a task",
{
task_id: {
type: "string",
description: "Task ID",
required: true,
},
},
async (args: any) => {
await client.delete(`/task/${args.task_id}/`);
return {
content: [
{
type: "text",
text: `Task ${args.task_id} deleted successfully`,
},
],
};
}
);
// Complete task
server.tool(
"close_complete_task",
"Mark a task as complete",
{
task_id: {
type: "string",
description: "Task ID",
required: true,
},
},
async (args: any) => {
const task = await client.put<Task>(`/task/${args.task_id}/`, {
is_complete: true,
});
return {
content: [
{
type: "text",
text: JSON.stringify(task, null, 2),
},
],
};
}
);
}

View File

@ -0,0 +1,84 @@
// User and role management tools
import type { CloseClient } from "../client/close-client.js";
import type { User, Role } from "../types/index.js";
export function registerUsersTools(server: any, client: CloseClient) {
// List users
server.tool(
"close_list_users",
"List all users in the organization",
{},
async () => {
const users = await client.get<{ data: User[] }>("/user/");
return {
content: [
{
type: "text",
text: JSON.stringify(users, null, 2),
},
],
};
}
);
// Get user
server.tool(
"close_get_user",
"Get a specific user by ID",
{
user_id: {
type: "string",
description: "User ID",
required: true,
},
},
async (args: any) => {
const user = await client.get<User>(`/user/${args.user_id}/`);
return {
content: [
{
type: "text",
text: JSON.stringify(user, null, 2),
},
],
};
}
);
// Get current user
server.tool(
"close_get_current_user",
"Get the current authenticated user",
{},
async () => {
const user = await client.get<User>("/me/");
return {
content: [
{
type: "text",
text: JSON.stringify(user, null, 2),
},
],
};
}
);
// List roles
server.tool(
"close_list_roles",
"List all roles in the organization",
{},
async () => {
const roles = await client.get<{ data: Role[] }>("/role/");
return {
content: [
{
type: "text",
text: JSON.stringify(roles, null, 2),
},
],
};
}
);
}

View File

@ -0,0 +1,323 @@
// Close CRM Types
export interface CloseConfig {
apiKey: string;
baseUrl?: string;
}
export interface PaginationParams {
limit?: number;
skip?: number;
_cursor?: string;
}
export interface SearchParams extends PaginationParams {
query?: string;
_fields?: string[];
}
// Lead Types
export interface Lead {
id: string;
name: string;
display_name?: string;
status_id?: string;
status_label?: string;
description?: string;
url?: string;
created_by?: string;
created_by_name?: string;
date_created?: string;
date_updated?: string;
organization_id?: string;
contacts?: Contact[];
opportunities?: Opportunity[];
tasks?: Task[];
custom?: Record<string, any>;
addresses?: Address[];
}
export interface Address {
label?: string;
address_1?: string;
address_2?: string;
city?: string;
state?: string;
zipcode?: string;
country?: string;
}
// Contact Types
export interface Contact {
id: string;
lead_id?: string;
name?: string;
title?: string;
emails?: EmailAddress[];
phones?: Phone[];
urls?: Url[];
date_created?: string;
date_updated?: string;
organization_id?: string;
created_by?: string;
}
export interface EmailAddress {
email: string;
type?: string;
}
export interface Phone {
phone: string;
type?: string;
country?: string;
}
export interface Url {
url: string;
type?: string;
}
// Opportunity Types
export interface Opportunity {
id: string;
lead_id: string;
status_id: string;
status_type?: string;
status_label?: string;
note?: string;
confidence?: number;
value?: number;
value_period?: string;
value_currency?: string;
date_created?: string;
date_updated?: string;
date_won?: string;
user_id?: string;
user_name?: string;
organization_id?: string;
}
export interface OpportunityStatus {
id: string;
label: string;
type: string;
}
export interface Pipeline {
id: string;
name: string;
organization_id?: string;
statuses?: OpportunityStatus[];
}
// Activity Types
export interface Activity {
id: string;
lead_id?: string;
contact_id?: string;
user_id?: string;
user_name?: string;
date_created?: string;
date_updated?: string;
organization_id?: string;
_type?: string;
}
export interface Note extends Activity {
note: string;
}
export interface Call extends Activity {
duration?: number;
phone?: string;
direction?: string;
disposition?: string;
note?: string;
recording_url?: string;
}
export interface Email extends Activity {
subject?: string;
body_text?: string;
body_html?: string;
status?: string;
direction?: string;
sender?: string;
to?: string[];
cc?: string[];
bcc?: string[];
opens?: number;
clicks?: number;
}
export interface SMS extends Activity {
text: string;
direction?: string;
status?: string;
remote_phone?: string;
local_phone?: string;
}
export interface Meeting extends Activity {
title?: string;
starts_at?: string;
ends_at?: string;
location?: string;
note?: string;
attendees?: string[];
}
// Task Types
export interface Task {
id: string;
_type: string;
assigned_to?: string;
assigned_to_name?: string;
lead_id?: string;
lead_name?: string;
text?: string;
date?: string;
is_complete?: boolean;
date_created?: string;
date_updated?: string;
organization_id?: string;
}
// Smart View Types
export interface SmartView {
id: string;
name: string;
query?: string;
type?: string;
date_created?: string;
date_updated?: string;
organization_id?: string;
}
// User Types
export interface User {
id: string;
email: string;
first_name?: string;
last_name?: string;
image?: string;
date_created?: string;
date_updated?: string;
organizations?: any[];
}
export interface Role {
id: string;
name: string;
organization_id?: string;
}
// Custom Field Types
export interface CustomField {
id: string;
name: string;
type: string;
required?: boolean;
accepts_multiple_values?: boolean;
editable_with_roles?: string[];
choices?: string[];
date_created?: string;
date_updated?: string;
organization_id?: string;
}
// Sequence Types
export interface Sequence {
id: string;
name: string;
status?: string;
max_activations?: number;
throttle_capacity?: number;
throttle_period_seconds?: number;
date_created?: string;
date_updated?: string;
organization_id?: string;
}
export interface SequenceSubscription {
id: string;
sequence_id: string;
lead_id: string;
contact_id?: string;
sender_account_id?: string;
status?: string;
date_created?: string;
date_paused?: string;
date_completed?: string;
}
// Bulk Operation Types
export interface BulkEditRequest {
lead_ids?: string[];
query?: string;
updates: Record<string, any>;
}
export interface BulkDeleteRequest {
lead_ids?: string[];
query?: string;
}
export interface BulkEmailRequest {
lead_ids?: string[];
query?: string;
template_id?: string;
subject: string;
body: string;
sender?: string;
}
// Reporting Types
export interface LeadStatusChange {
lead_id: string;
lead_name?: string;
old_status?: string;
new_status?: string;
changed_by?: string;
date_changed?: string;
}
export interface OpportunityFunnel {
status_id: string;
status_label?: string;
count: number;
total_value?: number;
average_value?: number;
}
export interface ActivitySummary {
date: string;
calls?: number;
emails?: number;
meetings?: number;
notes?: number;
total?: number;
}
export interface RevenueForcast {
period: string;
pipeline_value?: number;
weighted_value?: number;
won_value?: number;
closed_count?: number;
}
// API Response Types
export interface CloseAPIResponse<T> {
data?: T[];
has_more?: boolean;
total_results?: number;
cursor?: string;
}
export interface CloseAPIError {
error?: string;
errors?: any[];
field_errors?: Record<string, string[]>;
}

View File

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

View File

@ -1,231 +1,169 @@
# FieldEdge MCP Server # FieldEdge MCP Server
Complete Model Context Protocol (MCP) server for FieldEdge field service management platform. Provides comprehensive access to jobs, customers, invoices, estimates, technicians, dispatch, equipment, inventory, service agreements, and reporting. Complete MCP server for FieldEdge field service management platform with 87+ tools and 16 React apps.
## Features ## Features
### 45+ Tools Across 10 Categories ### 87+ Tools Across 13 Domains
#### Jobs Management (9 tools) - **Customer Management** (10 tools): Create, update, search customers, manage balances, view history
- `fieldedge_jobs_list` - List and filter jobs - **Job Management** (9 tools): Full CRUD operations, start/complete/cancel jobs, assign technicians
- `fieldedge_jobs_get` - Get job details - **Invoice Management** (9 tools): Create, send, void invoices, record payments, generate PDFs
- `fieldedge_jobs_create` - Create new job - **Estimate Management** (8 tools): Create quotes, send to customers, approve, convert to invoices
- `fieldedge_jobs_update` - Update job - **Equipment Management** (7 tools): Track equipment, service history, schedule maintenance
- `fieldedge_jobs_complete` - Mark job complete - **Technician Management** (9 tools): Manage technicians, schedules, availability, time tracking
- `fieldedge_jobs_cancel` - Cancel job - **Scheduling & Dispatch** (8 tools): Create appointments, dispatch board, route optimization
- `fieldedge_jobs_line_items_list` - List job line items - **Inventory Management** (7 tools): Track parts, adjust quantities, low stock alerts
- `fieldedge_jobs_line_items_add` - Add line item to job - **Payment Management** (5 tools): Process payments, refunds, payment history
- `fieldedge_jobs_equipment_list` - List equipment on job - **Reporting & Analytics** (8 tools): Revenue, productivity, aging receivables, satisfaction metrics
- **Location Management** (5 tools): Manage customer service locations
- **Service Agreements** (6 tools): Maintenance contracts, renewals, cancellations
- **Task Management** (6 tools): Follow-ups, to-dos, task completion
#### Customer Management (8 tools) ### 16 React MCP Apps
- `fieldedge_customers_list` - List and filter customers
- `fieldedge_customers_get` - Get customer details
- `fieldedge_customers_create` - Create new customer
- `fieldedge_customers_update` - Update customer
- `fieldedge_customers_delete` - Delete/deactivate customer
- `fieldedge_customers_search` - Search customers
- `fieldedge_customers_locations_list` - List customer locations
- `fieldedge_customers_equipment_list` - List customer equipment
#### Invoice Management (6 tools) Modern dark-themed React applications built with Vite:
- `fieldedge_invoices_list` - List and filter invoices
- `fieldedge_invoices_get` - Get invoice details
- `fieldedge_invoices_create` - Create invoice
- `fieldedge_invoices_update` - Update invoice
- `fieldedge_invoices_payments_list` - List payments
- `fieldedge_invoices_payments_add` - Add payment
#### Estimate Management (6 tools) 1. **Dashboard** - Key metrics and recent activity
- `fieldedge_estimates_list` - List estimates 2. **Customer Management** - Browse and manage customers
- `fieldedge_estimates_get` - Get estimate details 3. **Job Management** - View and manage jobs/work orders
- `fieldedge_estimates_create` - Create estimate 4. **Scheduling & Dispatch** - Dispatch board and appointment scheduling
- `fieldedge_estimates_update` - Update estimate 5. **Invoice Management** - Create and manage invoices
- `fieldedge_estimates_send` - Send estimate to customer 6. **Estimate Management** - Create and manage quotes
- `fieldedge_estimates_approve` - Approve and convert to job 7. **Technician Management** - Manage technicians and schedules
8. **Equipment Management** - Track customer equipment
#### Technician Management (6 tools) 9. **Inventory Management** - Parts and equipment inventory
- `fieldedge_technicians_list` - List technicians 10. **Payment Management** - Process payments
- `fieldedge_technicians_get` - Get technician details 11. **Service Agreements** - Maintenance contracts
- `fieldedge_technicians_create` - Create technician 12. **Reports & Analytics** - Business reports
- `fieldedge_technicians_update` - Update technician 13. **Task Management** - Follow-ups and to-dos
- `fieldedge_technicians_performance_get` - Get performance metrics 14. **Calendar View** - Appointment calendar
- `fieldedge_technicians_time_entries_list` - List time entries 15. **Map View** - Geographic view of jobs
16. **Price Book** - Service and part pricing
#### Dispatch Management (5 tools)
- `fieldedge_dispatch_board_get` - Get dispatch board
- `fieldedge_dispatch_assign_tech` - Assign technician to job
- `fieldedge_dispatch_technician_availability_get` - Get technician availability
- `fieldedge_dispatch_zones_list` - List dispatch zones
- `fieldedge_dispatch_optimize` - Auto-optimize dispatch schedule
#### Equipment Management (5 tools)
- `fieldedge_equipment_list` - List equipment
- `fieldedge_equipment_get` - Get equipment details
- `fieldedge_equipment_create` - Create equipment record
- `fieldedge_equipment_update` - Update equipment
- `fieldedge_equipment_service_history_list` - List service history
#### Inventory Management (6 tools)
- `fieldedge_inventory_parts_list` - List inventory parts
- `fieldedge_inventory_parts_get` - Get part details
- `fieldedge_inventory_stock_update` - Update stock levels
- `fieldedge_inventory_purchase_orders_list` - List purchase orders
- `fieldedge_inventory_purchase_orders_get` - Get PO details
- `fieldedge_inventory_reorder_report` - Get reorder report
#### Service Agreements (6 tools)
- `fieldedge_agreements_list` - List service agreements
- `fieldedge_agreements_get` - Get agreement details
- `fieldedge_agreements_create` - Create agreement
- `fieldedge_agreements_update` - Update agreement
- `fieldedge_agreements_cancel` - Cancel agreement
- `fieldedge_agreements_renew` - Renew agreement
#### Reporting & Analytics (6 tools)
- `fieldedge_reports_revenue` - Revenue report
- `fieldedge_reports_job_profitability` - Job profitability analysis
- `fieldedge_reports_technician_performance` - Tech performance metrics
- `fieldedge_reports_aging` - A/R aging report
- `fieldedge_reports_service_agreement_revenue` - Agreement revenue
- `fieldedge_reports_equipment_service_due` - Equipment service due
### 16 Interactive MCP Apps
- **job-dashboard** - Interactive jobs overview with filtering
- **job-detail** - Detailed job view with all information
- **job-grid** - Spreadsheet-style bulk job management
- **customer-detail** - Complete customer profile
- **customer-grid** - Customer data grid view
- **invoice-dashboard** - Invoice and payment tracking
- **estimate-builder** - Interactive estimate creation
- **dispatch-board** - Visual dispatch board
- **schedule-calendar** - Job scheduling calendar
- **technician-dashboard** - Tech performance dashboard
- **equipment-tracker** - Equipment and maintenance tracking
- **inventory-manager** - Inventory and stock management
- **agreement-manager** - Service agreement management
- **revenue-dashboard** - Revenue analytics
- **performance-metrics** - Performance analytics
- **aging-report** - A/R aging visualization
## Installation ## Installation
```bash ```bash
npm install npm install
npm run build
``` ```
## Configuration ## Configuration
Set the following environment variables: Create a `.env` file with your FieldEdge credentials:
```bash ```env
export FIELDEDGE_API_KEY="your_api_key_here" FIELDEDGE_API_KEY=your_api_key_here
export FIELDEDGE_BASE_URL="https://api.fieldedge.com/v2" # Optional, defaults to production FIELDEDGE_API_URL=https://api.fieldedge.com/v1
FIELDEDGE_COMPANY_ID=your_company_id
FIELDEDGE_TIMEOUT=30000
``` ```
## Usage ## Usage
### With Claude Desktop ### As MCP Server
Add to your `claude_desktop_config.json`: Add to your MCP settings:
```json ```json
{ {
"mcpServers": { "mcpServers": {
"fieldedge": { "fieldedge": {
"command": "node", "command": "npx",
"args": ["/path/to/fieldedge/dist/main.js"], "args": ["-y", "@mcpengine/fieldedge-mcp-server"],
"env": { "env": {
"FIELDEDGE_API_KEY": "your_api_key_here" "FIELDEDGE_API_KEY": "your_api_key"
} }
} }
} }
} }
``` ```
### Standalone ### Development
```bash ```bash
FIELDEDGE_API_KEY=your_key npm start # Build TypeScript
npm run build
# Watch mode
npm run dev
# Build React apps
npm run build:ui
``` ```
## Example Queries ## API Coverage
**Jobs:** The server provides comprehensive coverage of the FieldEdge API:
- "Show me all emergency jobs scheduled for today"
- "Create a new HVAC maintenance job for customer C123"
- "What jobs are assigned to technician T456?"
- "Mark job J789 as complete"
**Customers:** - Customer and contact management
- "Find all commercial customers in Chicago" - Job/work order lifecycle management
- "Show me customer details for account C123" - Scheduling and dispatch operations
- "List all equipment for customer C456" - Invoicing and payment processing
- Estimate/quote generation
**Invoices:** - Equipment and asset tracking
- "Show me all overdue invoices" - Inventory management
- "Create an invoice for job J789" - Technician management and time tracking
- "Add a $500 payment to invoice INV-123" - Service agreement management
- Business reporting and analytics
**Dispatch:**
- "Show me today's dispatch board"
- "Assign technician T123 to job J456"
- "What's the technician availability for tomorrow?"
- "Optimize the dispatch schedule for tomorrow"
**Reports:**
- "Show me revenue for the last 30 days"
- "What's the profitability of job J123?"
- "Generate an aging report"
- "Show technician performance metrics for this month"
## Architecture ## Architecture
``` ```
src/ src/
├── client.ts # API client with auth, pagination, error handling ├── clients/
├── types.ts # TypeScript type definitions │ └── fieldedge.ts # API client with auth, pagination, error handling
├── tools/ # Tool implementations ├── types/
│ ├── jobs-tools.ts │ └── index.ts # TypeScript type definitions
│ ├── customers-tools.ts ├── tools/
│ ├── invoices-tools.ts │ ├── customers.ts # Customer management tools
│ ├── estimates-tools.ts │ ├── jobs.ts # Job management tools
│ ├── technicians-tools.ts │ ├── invoices.ts # Invoice management tools
│ ├── dispatch-tools.ts │ ├── estimates.ts # Estimate management tools
│ ├── equipment-tools.ts │ ├── equipment.ts # Equipment management tools
│ ├── inventory-tools.ts │ ├── technicians.ts # Technician management tools
│ ├── agreements-tools.ts │ ├── scheduling.ts # Scheduling and dispatch tools
│ └── reporting-tools.ts │ ├── inventory.ts # Inventory management tools
├── apps/ # MCP app implementations │ ├── payments.ts # Payment management tools
│ └── index.ts │ ├── reporting.ts # Reporting and analytics tools
├── server.ts # MCP server setup │ ├── locations.ts # Location management tools
└── main.ts # Entry point │ ├── service-agreements.ts # Service agreement tools
│ └── tasks.ts # Task management tools
├── ui/ # React MCP apps (16 apps)
│ ├── dashboard/
│ ├── customers/
│ ├── jobs/
│ └── ...
├── server.ts # MCP server implementation
└── main.ts # Entry point
``` ```
## API Client Features ## Error Handling
- **Bearer Token Authentication** - Automatic authorization header injection The client includes comprehensive error handling:
- **Pagination Support** - Built-in pagination handling with `getPaginated()` and `getAllPages()`
- **Error Handling** - Comprehensive error handling with detailed error messages
- **Request Methods** - Full REST support (GET, POST, PUT, PATCH, DELETE)
- **Type Safety** - Full TypeScript typing for all API responses
## Development - Authentication errors (401)
- Permission errors (403)
- Not found errors (404)
- Rate limiting (429)
- Server errors (5xx)
- Network errors
```bash ## Rate Limiting
# Install dependencies
npm install
# Build Automatic rate limit tracking and retry logic included. The client monitors rate limit headers and automatically waits when limits are approached.
npm run build
# Development with watch mode ## TypeScript Support
npm run build -- --watch
# Run tests (if implemented) Full TypeScript support with comprehensive type definitions for:
npm test
``` - All API request/response types
- Tool input schemas
- Error types
- Configuration options
## Contributing
Issues and pull requests welcome at [github.com/BusyBee3333/mcpengine](https://github.com/BusyBee3333/mcpengine)
## License ## License
@ -233,6 +171,4 @@ MIT
## Support ## Support
For issues or questions: For API access and documentation, visit [docs.api.fieldedge.com](https://docs.api.fieldedge.com)
- FieldEdge API documentation: https://developer.fieldedge.com
- MCP Protocol: https://modelcontextprotocol.io

View File

@ -1,31 +1,42 @@
{ {
"name": "@mcpengine/fieldedge", "name": "@mcpengine/fieldedge-mcp-server",
"version": "1.0.0", "version": "1.0.0",
"description": "FieldEdge MCP Server - Complete field service management integration", "description": "MCP server for FieldEdge field service management platform",
"keywords": ["mcp", "fieldedge", "field-service", "hvac", "plumbing", "electrical", "contractors"],
"author": "MCP Engine",
"license": "MIT",
"type": "module", "type": "module",
"main": "dist/main.js",
"bin": { "bin": {
"fieldedge-mcp": "./dist/main.js" "fieldedge-mcp": "./dist/main.js"
}, },
"scripts": { "files": [
"build": "tsc", "dist",
"start": "node dist/main.js" "README.md"
},
"keywords": [
"mcp",
"fieldedge",
"field-service",
"hvac",
"dispatch",
"service-management"
], ],
"author": "MCPEngine", "scripts": {
"license": "MIT", "build": "tsc && npm run build:ui",
"build:ui": "node scripts/build-ui.js",
"dev": "tsc --watch",
"prepublishOnly": "npm run build",
"test": "echo 'Tests coming soon' && exit 0"
},
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4" "@modelcontextprotocol/sdk": "^1.0.4",
"axios": "^1.7.9",
"dotenv": "^16.4.7",
"zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
"typescript": "^5.7.2" "typescript": "^5.7.3",
"vite": "^6.0.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@types/react": "^19.0.6",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4"
},
"engines": {
"node": ">=18.0.0"
} }
} }

View File

@ -1,45 +1,41 @@
#!/usr/bin/env node #!/usr/bin/env node
/**
* Build script for React UI apps
*/
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { readdirSync, statSync } from 'fs'; import { readdirSync, statSync } from 'fs';
import { join } from 'path'; import { join, dirname } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(fileURLToPath(import.meta.url));
const __dirname = dirname(__filename); const uiDir = join(__dirname, '../src/ui');
const reactAppDir = join(__dirname, '..', 'src', 'ui', 'react-app'); console.log('Building React apps...\n');
try { const apps = readdirSync(uiDir).filter((file) => {
const apps = readdirSync(reactAppDir).filter(file => { const fullPath = join(uiDir, file);
const fullPath = join(reactAppDir, file); return statSync(fullPath).isDirectory();
return statSync(fullPath).isDirectory(); });
});
console.log(`Found ${apps.length} React apps to build`); let successCount = 0;
let failCount = 0;
for (const app of apps) { for (const app of apps) {
const appPath = join(reactAppDir, app); const appPath = join(uiDir, app);
console.log(`Building ${app}...`); console.log(`Building ${app}...`);
try { try {
execSync('npx vite build', { execSync('npx vite build', {
cwd: appPath, cwd: appPath,
stdio: 'inherit', stdio: 'inherit',
}); });
console.log(`${app} built successfully`); successCount++;
} catch (error) { } catch (error) {
console.error(`Failed to build ${app}`); console.error(`Failed to build ${app}`);
} failCount++;
} }
}
console.log('UI build complete'); console.log(`\n✅ Successfully built ${successCount} apps`);
} catch (error) { if (failCount > 0) {
console.error('Build failed:', error.message); console.log(`❌ Failed to build ${failCount} apps`);
process.exit(1); process.exit(1);
} }

View File

@ -0,0 +1,256 @@
#!/usr/bin/env node
import { writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const uiDir = join(__dirname, '../src/ui');
const apps = [
{ name: 'customers', title: 'Customer Management', description: 'Browse and manage customers' },
{ name: 'jobs', title: 'Job Management', description: 'View and manage jobs/work orders' },
{ name: 'scheduling', title: 'Scheduling & Dispatch', description: 'Dispatch board and appointment scheduling' },
{ name: 'invoices', title: 'Invoice Management', description: 'Create and manage invoices' },
{ name: 'estimates', title: 'Estimate Management', description: 'Create and manage estimates/quotes' },
{ name: 'technicians', title: 'Technician Management', description: 'Manage technicians and schedules' },
{ name: 'equipment', title: 'Equipment Management', description: 'Track customer equipment' },
{ name: 'inventory', title: 'Inventory Management', description: 'Manage parts and equipment inventory' },
{ name: 'payments', title: 'Payment Management', description: 'Process payments and view history' },
{ name: 'service-agreements', title: 'Service Agreements', description: 'Manage maintenance contracts' },
{ name: 'reports', title: 'Reports & Analytics', description: 'Business reports and analytics' },
{ name: 'tasks', title: 'Task Management', description: 'Manage follow-ups and to-dos' },
{ name: 'calendar', title: 'Calendar View', description: 'Calendar view of appointments' },
{ name: 'map-view', title: 'Map View', description: 'Map view of jobs and technicians' },
{ name: 'price-book', title: 'Price Book', description: 'Manage pricing for services' },
];
const createApp = (app) => {
const appDir = join(uiDir, app.name);
mkdirSync(appDir, { recursive: true });
// App.tsx
const appTsx = `import { useState, useEffect } from 'react';
import './styles.css';
export default function ${app.name.replace(/-/g, '')}App() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<any[]>([]);
useEffect(() => {
setTimeout(() => {
setData([
{ id: '1', name: 'Sample Item 1', status: 'active' },
{ id: '2', name: 'Sample Item 2', status: 'active' },
{ id: '3', name: 'Sample Item 3', status: 'pending' },
]);
setLoading(false);
}, 800);
}, []);
if (loading) {
return (
<div className="app">
<div className="loading">Loading ${app.title.toLowerCase()}...</div>
</div>
);
}
return (
<div className="app">
<header className="header">
<h1>${app.title}</h1>
<button className="btn-primary">+ Add New</button>
</header>
<div className="content">
<div className="section">
<h2>${app.description}</h2>
<div className="data-grid">
{data.map((item) => (
<div key={item.id} className="data-card">
<h3>{item.name}</h3>
<span className={\`badge badge-\${item.status}\`}>{item.status}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}
`;
// styles.css (shared dark theme)
const stylesCss = `* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f1419;
color: #e8eaed;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
font-weight: 600;
}
.btn-primary {
background: #4c9aff;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: #3d8aef;
}
.loading {
text-align: center;
padding: 4rem;
color: #9aa0a6;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section {
background: #1e2732;
border: 1px solid #2d3748;
border-radius: 8px;
padding: 1.5rem;
}
.section h2 {
font-size: 1.125rem;
margin-bottom: 1rem;
color: #e8eaed;
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.data-card {
background: #131920;
border: 1px solid #2d3748;
border-radius: 6px;
padding: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.2s;
}
.data-card:hover {
border-color: #4c9aff;
}
.data-card h3 {
font-size: 1rem;
font-weight: 500;
}
.badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-active {
background: #1a4d2e;
color: #4ade80;
}
.badge-pending {
background: #4a3810;
color: #fbbf24;
}
.badge-completed {
background: #1e3a8a;
color: #60a5fa;
}
`;
// main.tsx
const mainTsx = `import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
`;
// index.html
const indexHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${app.title} - FieldEdge</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>
`;
// vite.config.ts
const viteConfig = `import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: '../../../dist/ui/${app.name}',
emptyOutDir: true,
},
});
`;
writeFileSync(join(appDir, 'App.tsx'), appTsx);
writeFileSync(join(appDir, 'styles.css'), stylesCss);
writeFileSync(join(appDir, 'main.tsx'), mainTsx);
writeFileSync(join(appDir, 'index.html'), indexHtml);
writeFileSync(join(appDir, 'vite.config.ts'), viteConfig);
console.log(`Created app: ${app.name}`);
};
apps.forEach(createApp);
console.log(`\nSuccessfully created ${apps.length} React apps!`);

View File

@ -1,363 +0,0 @@
/**
* FieldEdge MCP Apps
*/
import { FieldEdgeClient } from '../client.js';
export function createApps(client: FieldEdgeClient) {
return [
// Job Management Apps
{
name: 'job-dashboard',
description: 'Interactive dashboard showing all jobs with filtering and sorting',
type: 'dashboard',
handler: async () => {
const jobs = await client.get('/jobs', { pageSize: 100 });
return {
type: 'ui',
title: 'Jobs Dashboard',
content: `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
.dashboard { max-width: 1400px; margin: 0 auto; }
h1 { color: #1a1a1a; margin-bottom: 24px; font-size: 28px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.stat-value { font-size: 32px; font-weight: bold; color: #2563eb; }
.stat-label { color: #666; margin-top: 8px; font-size: 14px; }
.filters { background: white; padding: 16px; border-radius: 12px; margin-bottom: 16px; display: flex; gap: 12px; flex-wrap: wrap; }
.filter-group { display: flex; flex-direction: column; gap: 4px; }
.filter-group label { font-size: 12px; color: #666; font-weight: 500; }
select, input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
.jobs-grid { display: grid; gap: 16px; }
.job-card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-left: 4px solid #2563eb; }
.job-card.emergency { border-left-color: #dc2626; }
.job-card.high { border-left-color: #f59e0b; }
.job-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px; }
.job-number { font-size: 18px; font-weight: bold; color: #1a1a1a; }
.status-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
.status-scheduled { background: #dbeafe; color: #1e40af; }
.status-in_progress { background: #fef3c7; color: #92400e; }
.status-completed { background: #d1fae5; color: #065f46; }
.job-details { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; color: #666; font-size: 14px; }
.detail-item { display: flex; flex-direction: column; gap: 4px; }
.detail-label { font-size: 12px; color: #999; }
.detail-value { color: #1a1a1a; font-weight: 500; }
</style>
</head>
<body>
<div class="dashboard">
<h1>📋 Jobs Dashboard</h1>
<div class="stats">
<div class="stat-card">
<div class="stat-value" id="total-jobs">0</div>
<div class="stat-label">Total Jobs</div>
</div>
<div class="stat-card">
<div class="stat-value" id="scheduled-jobs">0</div>
<div class="stat-label">Scheduled</div>
</div>
<div class="stat-card">
<div class="stat-value" id="in-progress-jobs">0</div>
<div class="stat-label">In Progress</div>
</div>
<div class="stat-card">
<div class="stat-value" id="completed-jobs">0</div>
<div class="stat-label">Completed Today</div>
</div>
</div>
<div class="filters">
<div class="filter-group">
<label>Status</label>
<select id="status-filter">
<option value="">All</option>
<option value="scheduled">Scheduled</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="on_hold">On Hold</option>
</select>
</div>
<div class="filter-group">
<label>Priority</label>
<select id="priority-filter">
<option value="">All</option>
<option value="emergency">Emergency</option>
<option value="high">High</option>
<option value="normal">Normal</option>
<option value="low">Low</option>
</select>
</div>
<div class="filter-group">
<label>Search</label>
<input type="text" id="search" placeholder="Job #, customer...">
</div>
</div>
<div class="jobs-grid" id="jobs-container"></div>
</div>
<script>
const data = ${JSON.stringify(jobs)};
const jobsContainer = document.getElementById('jobs-container');
function renderJobs(jobs) {
jobsContainer.innerHTML = jobs.map(job => \`
<div class="job-card \${job.priority}">
<div class="job-header">
<div class="job-number">#\${job.jobNumber}</div>
<span class="status-badge status-\${job.status}">\${job.status.replace('_', ' ').toUpperCase()}</span>
</div>
<div class="job-details">
<div class="detail-item">
<span class="detail-label">Customer</span>
<span class="detail-value">\${job.customerName || 'N/A'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Technician</span>
<span class="detail-value">\${job.assignedTechName || 'Unassigned'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Scheduled</span>
<span class="detail-value">\${job.scheduledStart ? new Date(job.scheduledStart).toLocaleString() : 'TBD'}</span>
</div>
<div class="detail-item">
<span class="detail-label">Priority</span>
<span class="detail-value">\${job.priority.toUpperCase()}</span>
</div>
</div>
</div>
\`).join('');
// Update stats
document.getElementById('total-jobs').textContent = jobs.length;
document.getElementById('scheduled-jobs').textContent = jobs.filter(j => j.status === 'scheduled').length;
document.getElementById('in-progress-jobs').textContent = jobs.filter(j => j.status === 'in_progress').length;
document.getElementById('completed-jobs').textContent = jobs.filter(j => j.status === 'completed').length;
}
renderJobs(data.data || []);
</script>
</body>
</html>
`.trim(),
};
},
},
{
name: 'job-detail',
description: 'Detailed view of a specific job with all information',
type: 'detail',
handler: async (jobId: string) => {
const job = await client.get(`/jobs/${jobId}`);
const lineItems = await client.get(`/jobs/${jobId}/line-items`);
return {
type: 'ui',
title: `Job #${(job as any).jobNumber}`,
content: `Job details for ${jobId} with line items...`,
};
},
},
{
name: 'job-grid',
description: 'Spreadsheet-style grid view of jobs for bulk operations',
type: 'grid',
handler: async () => {
const jobs = await client.get('/jobs', { pageSize: 200 });
return {
type: 'ui',
title: 'Jobs Grid',
content: 'Table/grid view of all jobs...',
};
},
},
// Customer Apps
{
name: 'customer-detail',
description: 'Complete customer profile with history, equipment, and agreements',
type: 'detail',
handler: async (customerId: string) => {
const customer = await client.get(`/customers/${customerId}`);
return {
type: 'ui',
title: `Customer: ${(customer as any).firstName} ${(customer as any).lastName}`,
content: 'Customer profile...',
};
},
},
{
name: 'customer-grid',
description: 'Grid view of all customers',
type: 'grid',
handler: async () => {
const customers = await client.get('/customers', { pageSize: 200 });
return {
type: 'ui',
title: 'Customers Grid',
content: 'Customer table...',
};
},
},
// Financial Apps
{
name: 'invoice-dashboard',
description: 'Invoice management dashboard with payment tracking',
type: 'dashboard',
handler: async () => {
const invoices = await client.get('/invoices', { pageSize: 100 });
return {
type: 'ui',
title: 'Invoices Dashboard',
content: 'Invoice dashboard...',
};
},
},
{
name: 'estimate-builder',
description: 'Interactive estimate creation and editing tool',
type: 'builder',
handler: async () => {
return {
type: 'ui',
title: 'Estimate Builder',
content: 'Estimate builder UI...',
};
},
},
// Dispatch Apps
{
name: 'dispatch-board',
description: 'Visual dispatch board showing technicians and job assignments',
type: 'board',
handler: async (date?: string) => {
const board = await client.get('/dispatch/board', { date: date || new Date().toISOString().split('T')[0] });
return {
type: 'ui',
title: 'Dispatch Board',
content: 'Dispatch board visualization...',
};
},
},
{
name: 'schedule-calendar',
description: 'Calendar view of scheduled jobs and technician availability',
type: 'calendar',
handler: async () => {
return {
type: 'ui',
title: 'Schedule Calendar',
content: 'Calendar view...',
};
},
},
// Technician Apps
{
name: 'technician-dashboard',
description: 'Technician performance and schedule dashboard',
type: 'dashboard',
handler: async () => {
const technicians = await client.get('/technicians');
return {
type: 'ui',
title: 'Technician Dashboard',
content: 'Technician metrics...',
};
},
},
// Equipment Apps
{
name: 'equipment-tracker',
description: 'Equipment tracking with service history and maintenance schedules',
type: 'tracker',
handler: async () => {
const equipment = await client.get('/equipment', { pageSize: 200 });
return {
type: 'ui',
title: 'Equipment Tracker',
content: 'Equipment list with service tracking...',
};
},
},
// Inventory Apps
{
name: 'inventory-manager',
description: 'Inventory management with stock levels and reorder alerts',
type: 'manager',
handler: async () => {
const parts = await client.get('/inventory/parts', { pageSize: 200 });
return {
type: 'ui',
title: 'Inventory Manager',
content: 'Inventory management...',
};
},
},
// Agreement Apps
{
name: 'agreement-manager',
description: 'Service agreement management and renewal tracking',
type: 'manager',
handler: async () => {
const agreements = await client.get('/agreements', { pageSize: 100 });
return {
type: 'ui',
title: 'Agreement Manager',
content: 'Service agreements...',
};
},
},
// Reporting Apps
{
name: 'revenue-dashboard',
description: 'Revenue analytics and trends',
type: 'dashboard',
handler: async () => {
const report = await client.get('/reports/revenue', {
startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
endDate: new Date().toISOString(),
});
return {
type: 'ui',
title: 'Revenue Dashboard',
content: 'Revenue charts and metrics...',
};
},
},
{
name: 'performance-metrics',
description: 'Technician and job performance metrics',
type: 'metrics',
handler: async () => {
return {
type: 'ui',
title: 'Performance Metrics',
content: 'Performance analytics...',
};
},
},
{
name: 'aging-report',
description: 'Accounts receivable aging report',
type: 'report',
handler: async () => {
const report = await client.get('/reports/aging');
return {
type: 'ui',
title: 'A/R Aging Report',
content: 'Aging report...',
};
},
},
];
}

View File

@ -1,215 +0,0 @@
/**
* FieldEdge API Client
* Handles authentication, pagination, and error handling
*/
import { FieldEdgeConfig, PaginatedResponse, ApiError } from './types.js';
export class FieldEdgeClient {
private apiKey: string;
private baseUrl: string;
constructor(config: FieldEdgeConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://api.fieldedge.com/v2';
}
/**
* Make a GET request with pagination support
*/
async get<T>(
endpoint: string,
params?: Record<string, unknown>
): Promise<T> {
const url = this.buildUrl(endpoint, params);
return this.request<T>('GET', url);
}
/**
* Make a paginated GET request
*/
async getPaginated<T>(
endpoint: string,
params?: Record<string, unknown>
): Promise<PaginatedResponse<T>> {
const page = (params?.page as number) || 1;
const pageSize = (params?.pageSize as number) || 50;
const response = await this.get<{
data: T[];
total: number;
page: number;
pageSize: number;
}>(endpoint, { ...params, page, pageSize });
return {
data: response.data,
total: response.total,
page: response.page,
pageSize: response.pageSize,
hasMore: page * pageSize < response.total,
};
}
/**
* Make a POST request
*/
async post<T>(endpoint: string, data?: unknown): Promise<T> {
const url = this.buildUrl(endpoint);
return this.request<T>('POST', url, data);
}
/**
* Make a PUT request
*/
async put<T>(endpoint: string, data?: unknown): Promise<T> {
const url = this.buildUrl(endpoint);
return this.request<T>('PUT', url, data);
}
/**
* Make a PATCH request
*/
async patch<T>(endpoint: string, data?: unknown): Promise<T> {
const url = this.buildUrl(endpoint);
return this.request<T>('PATCH', url, data);
}
/**
* Make a DELETE request
*/
async delete<T>(endpoint: string): Promise<T> {
const url = this.buildUrl(endpoint);
return this.request<T>('DELETE', url);
}
/**
* Core request method with error handling
*/
private async request<T>(
method: string,
url: string,
body?: unknown
): Promise<T> {
try {
const headers: HeadersInit = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
const options: RequestInit = {
method,
headers,
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
await this.handleErrorResponse(response);
}
// Handle 204 No Content
if (response.status === 204) {
return {} as T;
}
const data = await response.json();
return data as T;
} catch (error) {
if (error instanceof Error) {
throw this.createApiError(error.message, 0, error);
}
throw this.createApiError('Unknown error occurred', 0, error);
}
}
/**
* Handle error responses from the API
*/
private async handleErrorResponse(response: Response): Promise<never> {
let errorMessage = `API request failed: ${response.status} ${response.statusText}`;
let errorDetails: unknown;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorData.error || errorMessage;
errorDetails = errorData;
} catch {
// Response body is not JSON
}
throw this.createApiError(errorMessage, response.status, errorDetails);
}
/**
* Create a standardized API error
*/
private createApiError(
message: string,
statusCode: number,
details?: unknown
): Error {
const error = new Error(message) as Error & ApiError;
error.statusCode = statusCode;
error.details = details;
return error;
}
/**
* Build URL with query parameters
*/
private buildUrl(
endpoint: string,
params?: Record<string, unknown>
): string {
const url = new URL(
endpoint.startsWith('http') ? endpoint : `${this.baseUrl}${endpoint}`
);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
}
return url.toString();
}
/**
* Fetch all pages of a paginated endpoint
*/
async getAllPages<T>(
endpoint: string,
params?: Record<string, unknown>
): Promise<T[]> {
const allData: T[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await this.getPaginated<T>(endpoint, {
...params,
page,
});
allData.push(...response.data);
hasMore = response.hasMore;
page++;
// Safety limit to prevent infinite loops
if (page > 1000) {
console.warn('Reached maximum page limit (1000)');
break;
}
}
return allData;
}
}

View File

@ -0,0 +1,290 @@
/**
* FieldEdge API Client
* Handles authentication, rate limiting, pagination, and error handling
*/
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import type {
FieldEdgeConfig,
ApiResponse,
PaginatedResponse,
QueryParams,
} from '../types/index.js';
export class FieldEdgeAPIError extends Error {
constructor(
message: string,
public statusCode?: number,
public response?: any
) {
super(message);
this.name = 'FieldEdgeAPIError';
}
}
export class FieldEdgeClient {
private client: AxiosInstance;
private config: FieldEdgeConfig;
private rateLimitRemaining: number = 1000;
private rateLimitReset: number = 0;
constructor(config: FieldEdgeConfig) {
this.config = {
apiUrl: 'https://api.fieldedge.com/v1',
timeout: 30000,
...config,
};
this.client = axios.create({
baseURL: this.config.apiUrl,
timeout: this.config.timeout,
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
...(this.config.companyId && { 'X-Company-Id': this.config.companyId }),
},
});
// Request interceptor for rate limiting
this.client.interceptors.request.use(
async (config) => {
// Check rate limit
if (this.rateLimitRemaining <= 0 && Date.now() < this.rateLimitReset) {
const waitTime = this.rateLimitReset - Date.now();
console.warn(`Rate limit exceeded. Waiting ${waitTime}ms...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling and rate limit tracking
this.client.interceptors.response.use(
(response) => {
// Update rate limit info from headers
const remaining = response.headers['x-ratelimit-remaining'];
const reset = response.headers['x-ratelimit-reset'];
if (remaining) this.rateLimitRemaining = parseInt(remaining);
if (reset) this.rateLimitReset = parseInt(reset) * 1000;
return response;
},
(error: AxiosError) => {
if (error.response) {
const { status, data } = error.response;
// Handle specific error codes
switch (status) {
case 401:
throw new FieldEdgeAPIError('Authentication failed. Invalid API key.', 401, data);
case 403:
throw new FieldEdgeAPIError('Access forbidden. Check permissions.', 403, data);
case 404:
throw new FieldEdgeAPIError('Resource not found.', 404, data);
case 429:
throw new FieldEdgeAPIError('Rate limit exceeded.', 429, data);
case 500:
case 502:
case 503:
throw new FieldEdgeAPIError('FieldEdge server error. Please try again later.', status, data);
default:
const message = (data as any)?.message || (data as any)?.error || 'API request failed';
throw new FieldEdgeAPIError(message, status, data);
}
} else if (error.request) {
throw new FieldEdgeAPIError('No response from FieldEdge API. Check network connection.');
} else {
throw new FieldEdgeAPIError(`Request error: ${error.message}`);
}
}
);
}
/**
* Generic GET request with pagination support
*/
async get<T>(endpoint: string, params?: QueryParams): Promise<T> {
const response = await this.client.get(endpoint, { params });
return response.data;
}
/**
* Generic GET request for paginated data
*/
async getPaginated<T>(
endpoint: string,
params?: QueryParams
): Promise<PaginatedResponse<T>> {
const queryParams = {
page: params?.page || 1,
pageSize: params?.pageSize || 50,
...params,
};
const response = await this.client.get(endpoint, { params: queryParams });
return {
items: response.data.items || response.data.data || response.data,
total: response.data.total || response.data.items?.length || 0,
page: response.data.page || queryParams.page,
pageSize: response.data.pageSize || queryParams.pageSize,
totalPages: response.data.totalPages || Math.ceil((response.data.total || 0) / queryParams.pageSize),
};
}
/**
* Get all pages of paginated data
*/
async getAllPaginated<T>(
endpoint: string,
params?: QueryParams
): Promise<T[]> {
const allItems: T[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await this.getPaginated<T>(endpoint, {
...params,
page,
pageSize: params?.pageSize || 100,
});
allItems.push(...response.items);
hasMore = page < response.totalPages;
page++;
}
return allItems;
}
/**
* Generic POST request
*/
async post<T>(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post(endpoint, data, config);
return response.data;
}
/**
* Generic PUT request
*/
async put<T>(endpoint: string, data?: any): Promise<T> {
const response = await this.client.put(endpoint, data);
return response.data;
}
/**
* Generic PATCH request
*/
async patch<T>(endpoint: string, data?: any): Promise<T> {
const response = await this.client.patch(endpoint, data);
return response.data;
}
/**
* Generic DELETE request
*/
async delete<T>(endpoint: string): Promise<T> {
const response = await this.client.delete(endpoint);
return response.data;
}
/**
* Upload file
*/
async uploadFile(endpoint: string, file: Buffer, filename: string, mimeType?: string): Promise<any> {
const formData = new FormData();
const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength) as ArrayBuffer;
const blob = new Blob([arrayBuffer], { type: mimeType || 'application/octet-stream' });
formData.append('file', blob, filename);
const response = await this.client.post(endpoint, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
}
/**
* Download file
*/
async downloadFile(endpoint: string): Promise<Buffer> {
const response = await this.client.get(endpoint, {
responseType: 'arraybuffer',
});
return Buffer.from(response.data);
}
/**
* Test API connectivity
*/
async testConnection(): Promise<boolean> {
try {
await this.client.get('/health');
return true;
} catch (error) {
return false;
}
}
/**
* Get API usage/rate limit info
*/
getRateLimitInfo(): { remaining: number; resetAt: number } {
return {
remaining: this.rateLimitRemaining,
resetAt: this.rateLimitReset,
};
}
/**
* Update API configuration
*/
updateConfig(config: Partial<FieldEdgeConfig>): void {
this.config = { ...this.config, ...config };
if (config.apiKey) {
this.client.defaults.headers['Authorization'] = `Bearer ${config.apiKey}`;
}
if (config.companyId) {
this.client.defaults.headers['X-Company-Id'] = config.companyId;
}
if (config.apiUrl) {
this.client.defaults.baseURL = config.apiUrl;
}
if (config.timeout) {
this.client.defaults.timeout = config.timeout;
}
}
}
// Singleton instance
let clientInstance: FieldEdgeClient | null = null;
export function getFieldEdgeClient(config?: FieldEdgeConfig): FieldEdgeClient {
if (!clientInstance && config) {
clientInstance = new FieldEdgeClient(config);
}
if (!clientInstance) {
throw new Error('FieldEdge client not initialized. Provide API key configuration.');
}
return clientInstance;
}
export function initializeFieldEdgeClient(config: FieldEdgeConfig): FieldEdgeClient {
clientInstance = new FieldEdgeClient(config);
return clientInstance;
}

View File

@ -1,22 +1,35 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* FieldEdge MCP Server - Main Entry Point * FieldEdge MCP Server Entry Point
*/ */
import 'dotenv/config';
import { FieldEdgeServer } from './server.js'; import { FieldEdgeServer } from './server.js';
import { initializeFieldEdgeClient } from './clients/fieldedge.js';
async function main() { async function main() {
const apiKey = process.env.FIELDEDGE_API_KEY;
const baseUrl = process.env.FIELDEDGE_BASE_URL;
if (!apiKey) {
console.error('Error: FIELDEDGE_API_KEY environment variable is required');
process.exit(1);
}
try { try {
const server = new FieldEdgeServer(apiKey, baseUrl); // Get API key from environment
const apiKey = process.env.FIELDEDGE_API_KEY;
if (!apiKey) {
console.error('Error: FIELDEDGE_API_KEY environment variable is required');
console.error('Please set it in your environment or .env file');
process.exit(1);
}
// Initialize FieldEdge client
initializeFieldEdgeClient({
apiKey,
apiUrl: process.env.FIELDEDGE_API_URL,
companyId: process.env.FIELDEDGE_COMPANY_ID,
timeout: process.env.FIELDEDGE_TIMEOUT
? parseInt(process.env.FIELDEDGE_TIMEOUT)
: undefined,
});
// Start server
const server = new FieldEdgeServer();
await server.run(); await server.run();
} catch (error) { } catch (error) {
console.error('Fatal error:', error); console.error('Fatal error:', error);

View File

@ -7,56 +7,88 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { import {
CallToolRequestSchema, CallToolRequestSchema,
ListToolsRequestSchema, ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ErrorCode,
McpError,
} from '@modelcontextprotocol/sdk/types.js'; } from '@modelcontextprotocol/sdk/types.js';
import { initializeFieldEdgeClient } from './clients/fieldedge.js';
import { FieldEdgeClient } from './client.js'; // Import tool definitions and handlers
import { createJobsTools } from './tools/jobs-tools.js'; import { customerTools, handleCustomerTool } from './tools/customers.js';
import { createCustomersTools } from './tools/customers-tools.js'; import { jobTools, handleJobTool } from './tools/jobs.js';
import { createInvoicesTools } from './tools/invoices-tools.js'; import { invoiceTools, handleInvoiceTool } from './tools/invoices.js';
import { createEstimatesTools } from './tools/estimates-tools.js'; import { estimateTools, handleEstimateTool } from './tools/estimates.js';
import { createTechniciansTools } from './tools/technicians-tools.js'; import { equipmentTools, handleEquipmentTool } from './tools/equipment.js';
import { createDispatchTools } from './tools/dispatch-tools.js'; import { technicianTools, handleTechnicianTool } from './tools/technicians.js';
import { createEquipmentTools } from './tools/equipment-tools.js'; import { schedulingTools, handleSchedulingTool } from './tools/scheduling.js';
import { createInventoryTools } from './tools/inventory-tools.js'; import { inventoryTools, handleInventoryTool } from './tools/inventory.js';
import { createAgreementsTools } from './tools/agreements-tools.js'; import { paymentTools, handlePaymentTool } from './tools/payments.js';
import { createReportingTools } from './tools/reporting-tools.js'; import { reportingTools, handleReportingTool } from './tools/reporting.js';
import { createApps } from './apps/index.js'; import { locationTools, handleLocationTool } from './tools/locations.js';
import { serviceAgreementTools, handleServiceAgreementTool } from './tools/service-agreements.js';
import { taskTools, handleTaskTool } from './tools/tasks.js';
// Combine all tools
const ALL_TOOLS = [
...customerTools,
...jobTools,
...invoiceTools,
...estimateTools,
...equipmentTools,
...technicianTools,
...schedulingTools,
...inventoryTools,
...paymentTools,
...reportingTools,
...locationTools,
...serviceAgreementTools,
...taskTools,
];
// Tool handler map
const TOOL_HANDLERS: Record<string, (name: string, args: any) => Promise<any>> = {
// Customer tools
...Object.fromEntries(customerTools.map(t => [t.name, handleCustomerTool])),
// Job tools
...Object.fromEntries(jobTools.map(t => [t.name, handleJobTool])),
// Invoice tools
...Object.fromEntries(invoiceTools.map(t => [t.name, handleInvoiceTool])),
// Estimate tools
...Object.fromEntries(estimateTools.map(t => [t.name, handleEstimateTool])),
// Equipment tools
...Object.fromEntries(equipmentTools.map(t => [t.name, handleEquipmentTool])),
// Technician tools
...Object.fromEntries(technicianTools.map(t => [t.name, handleTechnicianTool])),
// Scheduling tools
...Object.fromEntries(schedulingTools.map(t => [t.name, handleSchedulingTool])),
// Inventory tools
...Object.fromEntries(inventoryTools.map(t => [t.name, handleInventoryTool])),
// Payment tools
...Object.fromEntries(paymentTools.map(t => [t.name, handlePaymentTool])),
// Reporting tools
...Object.fromEntries(reportingTools.map(t => [t.name, handleReportingTool])),
// Location tools
...Object.fromEntries(locationTools.map(t => [t.name, handleLocationTool])),
// Service agreement tools
...Object.fromEntries(serviceAgreementTools.map(t => [t.name, handleServiceAgreementTool])),
// Task tools
...Object.fromEntries(taskTools.map(t => [t.name, handleTaskTool])),
};
export class FieldEdgeServer { export class FieldEdgeServer {
private server: Server; private server: Server;
private client: FieldEdgeClient;
private tools: any[];
private apps: any[];
constructor(apiKey: string, baseUrl?: string) { constructor() {
this.client = new FieldEdgeClient({ apiKey, baseUrl });
// Initialize all tools
this.tools = [
...createJobsTools(this.client),
...createCustomersTools(this.client),
...createInvoicesTools(this.client),
...createEstimatesTools(this.client),
...createTechniciansTools(this.client),
...createDispatchTools(this.client),
...createEquipmentTools(this.client),
...createInventoryTools(this.client),
...createAgreementsTools(this.client),
...createReportingTools(this.client),
];
// Initialize all apps
this.apps = createApps(this.client);
// Create MCP server
this.server = new Server( this.server = new Server(
{ {
name: 'fieldedge', name: 'fieldedge-mcp-server',
version: '1.0.0', version: '1.0.0',
}, },
{ {
capabilities: { capabilities: {
tools: {}, tools: {},
resources: {},
}, },
} }
); );
@ -68,38 +100,174 @@ export class FieldEdgeServer {
// List available tools // List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => { this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return { return {
tools: this.tools.map((tool) => ({ tools: ALL_TOOLS,
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
}; };
}); });
// Handle tool calls // Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => { this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = this.tools.find((t) => t.name === request.params.name); const { name, arguments: args } = request.params;
if (!tool) {
throw new Error(`Unknown tool: ${request.params.name}`);
}
try { try {
const result = await tool.handler(request.params.arguments || {}); const handler = TOOL_HANDLERS[name];
return result; if (!handler) {
} catch (error) { throw new McpError(
if (error instanceof Error) { ErrorCode.MethodNotFound,
return { `Unknown tool: ${name}`
content: [ );
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
} }
throw error;
const result = await handler(name, args || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Tool execution failed: ${error.message}`
);
}
});
// List resources (React apps)
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'fieldedge://app/dashboard',
name: 'FieldEdge Dashboard',
description: 'Main dashboard with key metrics and recent activity',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/customers',
name: 'Customer Management',
description: 'Browse and manage customers',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/jobs',
name: 'Job Management',
description: 'View and manage jobs/work orders',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/scheduling',
name: 'Scheduling & Dispatch',
description: 'Dispatch board and appointment scheduling',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/invoices',
name: 'Invoice Management',
description: 'Create and manage invoices',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/estimates',
name: 'Estimate/Quote Management',
description: 'Create and manage estimates',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/technicians',
name: 'Technician Management',
description: 'Manage technicians and view schedules',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/equipment',
name: 'Equipment Management',
description: 'Track customer equipment and service history',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/inventory',
name: 'Inventory Management',
description: 'Manage parts and equipment inventory',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/payments',
name: 'Payment Management',
description: 'Process payments and view payment history',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/service-agreements',
name: 'Service Agreements',
description: 'Manage maintenance contracts and service plans',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/reports',
name: 'Reports & Analytics',
description: 'View business reports and analytics',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/tasks',
name: 'Task Management',
description: 'Manage follow-ups and to-do items',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/calendar',
name: 'Calendar View',
description: 'Calendar view of appointments and jobs',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/map-view',
name: 'Map View',
description: 'Map view of jobs and technician locations',
mimeType: 'text/html',
},
{
uri: 'fieldedge://app/price-book',
name: 'Price Book Management',
description: 'Manage pricing for services and parts',
mimeType: 'text/html',
},
],
};
});
// Read resource (serve React app HTML)
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
const appName = uri.replace('fieldedge://app/', '');
try {
// In production, this would read from dist/ui/{appName}/index.html
const htmlPath = `./dist/ui/${appName}/index.html`;
const fs = await import('fs/promises');
const html = await fs.readFile(htmlPath, 'utf-8');
return {
contents: [
{
uri,
mimeType: 'text/html',
text: html,
},
],
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to load app: ${appName}`
);
} }
}); });
} }

View File

@ -1,259 +0,0 @@
/**
* FieldEdge Service Agreements Tools
*/
import { FieldEdgeClient } from '../client.js';
import { ServiceAgreement, PaginationParams } from '../types.js';
export function createAgreementsTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_agreements_list',
description: 'List all service agreements/memberships',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
description: 'Filter by agreement status',
enum: ['active', 'cancelled', 'expired', 'suspended'],
},
customerId: { type: 'string', description: 'Filter by customer ID' },
type: { type: 'string', description: 'Filter by agreement type' },
expiringWithinDays: {
type: 'number',
description: 'Show agreements expiring within X days',
},
page: { type: 'number' },
pageSize: { type: 'number' },
},
},
handler: async (params: {
status?: string;
customerId?: string;
type?: string;
expiringWithinDays?: number;
}) => {
const result = await client.getPaginated<ServiceAgreement>(
'/agreements',
params
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_agreements_get',
description: 'Get detailed information about a specific service agreement',
inputSchema: {
type: 'object',
properties: {
agreementId: { type: 'string', description: 'Agreement ID' },
},
required: ['agreementId'],
},
handler: async (params: { agreementId: string }) => {
const agreement = await client.get<ServiceAgreement>(
`/agreements/${params.agreementId}`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(agreement, null, 2),
},
],
};
},
},
{
name: 'fieldedge_agreements_create',
description: 'Create a new service agreement',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
locationId: { type: 'string', description: 'Location ID' },
type: { type: 'string', description: 'Agreement type' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
billingFrequency: {
type: 'string',
description: 'Billing frequency',
enum: ['monthly', 'quarterly', 'annually'],
},
amount: { type: 'number', description: 'Billing amount' },
equipmentCovered: {
type: 'array',
items: { type: 'string' },
description: 'List of equipment IDs covered',
},
servicesCovered: {
type: 'array',
items: { type: 'string' },
description: 'List of services included',
},
visitsPerYear: {
type: 'number',
description: 'Number of included visits per year',
},
autoRenew: { type: 'boolean', description: 'Auto-renew on expiration' },
notes: { type: 'string', description: 'Agreement notes' },
},
required: ['customerId', 'type', 'startDate', 'billingFrequency', 'amount'],
},
handler: async (params: {
customerId: string;
locationId?: string;
type: string;
startDate: string;
endDate?: string;
billingFrequency: string;
amount: number;
equipmentCovered?: string[];
servicesCovered?: string[];
visitsPerYear?: number;
autoRenew?: boolean;
notes?: string;
}) => {
const agreement = await client.post<ServiceAgreement>('/agreements', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(agreement, null, 2),
},
],
};
},
},
{
name: 'fieldedge_agreements_update',
description: 'Update an existing service agreement',
inputSchema: {
type: 'object',
properties: {
agreementId: { type: 'string', description: 'Agreement ID' },
status: {
type: 'string',
enum: ['active', 'cancelled', 'expired', 'suspended'],
},
endDate: { type: 'string' },
amount: { type: 'number' },
equipmentCovered: {
type: 'array',
items: { type: 'string' },
},
servicesCovered: {
type: 'array',
items: { type: 'string' },
},
visitsPerYear: { type: 'number' },
autoRenew: { type: 'boolean' },
notes: { type: 'string' },
},
required: ['agreementId'],
},
handler: async (params: {
agreementId: string;
status?: string;
endDate?: string;
amount?: number;
equipmentCovered?: string[];
servicesCovered?: string[];
visitsPerYear?: number;
autoRenew?: boolean;
notes?: string;
}) => {
const { agreementId, ...updateData } = params;
const agreement = await client.patch<ServiceAgreement>(
`/agreements/${agreementId}`,
updateData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(agreement, null, 2),
},
],
};
},
},
{
name: 'fieldedge_agreements_cancel',
description: 'Cancel a service agreement',
inputSchema: {
type: 'object',
properties: {
agreementId: { type: 'string', description: 'Agreement ID' },
reason: { type: 'string', description: 'Cancellation reason' },
effectiveDate: {
type: 'string',
description: 'Effective cancellation date (ISO 8601)',
},
},
required: ['agreementId'],
},
handler: async (params: {
agreementId: string;
reason?: string;
effectiveDate?: string;
}) => {
const { agreementId, ...cancelData } = params;
const agreement = await client.post<ServiceAgreement>(
`/agreements/${agreementId}/cancel`,
cancelData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(agreement, null, 2),
},
],
};
},
},
{
name: 'fieldedge_agreements_renew',
description: 'Renew a service agreement',
inputSchema: {
type: 'object',
properties: {
agreementId: { type: 'string', description: 'Agreement ID' },
newStartDate: { type: 'string', description: 'New start date (ISO 8601)' },
newEndDate: { type: 'string', description: 'New end date (ISO 8601)' },
newAmount: { type: 'number', description: 'New billing amount' },
},
required: ['agreementId'],
},
handler: async (params: {
agreementId: string;
newStartDate?: string;
newEndDate?: string;
newAmount?: number;
}) => {
const { agreementId, ...renewData } = params;
const agreement = await client.post<ServiceAgreement>(
`/agreements/${agreementId}/renew`,
renewData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(agreement, null, 2),
},
],
};
},
},
];
}

View File

@ -1,234 +0,0 @@
/**
* FieldEdge Customers Tools
*/
import { FieldEdgeClient } from '../client.js';
import { Customer, CustomerLocation, Equipment, PaginationParams } from '../types.js';
export function createCustomersTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_customers_list',
description: 'List all customers with optional filtering',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
description: 'Filter by customer status',
enum: ['active', 'inactive'],
},
type: {
type: 'string',
description: 'Filter by customer type',
enum: ['residential', 'commercial'],
},
page: { type: 'number' },
pageSize: { type: 'number' },
},
},
handler: async (params: any) => {
const result = await client.getPaginated<Customer>('/customers', params as any);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_customers_get',
description: 'Get detailed information about a specific customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
},
required: ['customerId'],
},
handler: async (params: { customerId: string }) => {
const customer = await client.get<Customer>(`/customers/${params.customerId}`);
return {
content: [
{
type: 'text',
text: JSON.stringify(customer, null, 2),
},
],
};
},
},
{
name: 'fieldedge_customers_create',
description: 'Create a new customer',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
description: 'Customer type',
enum: ['residential', 'commercial'],
},
firstName: { type: 'string', description: 'First name (for residential)' },
lastName: { type: 'string', description: 'Last name (for residential)' },
companyName: { type: 'string', description: 'Company name (for commercial)' },
email: { type: 'string', description: 'Email address' },
phone: { type: 'string', description: 'Primary phone number' },
mobilePhone: { type: 'string', description: 'Mobile phone number' },
street1: { type: 'string', description: 'Street address line 1' },
street2: { type: 'string', description: 'Street address line 2' },
city: { type: 'string', description: 'City' },
state: { type: 'string', description: 'State' },
zip: { type: 'string', description: 'ZIP code' },
taxExempt: { type: 'boolean', description: 'Tax exempt status' },
},
required: ['type'],
},
handler: async (params: any) => {
const customer = await client.post<Customer>('/customers', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(customer, null, 2),
},
],
};
},
},
{
name: 'fieldedge_customers_update',
description: 'Update an existing customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
firstName: { type: 'string' },
lastName: { type: 'string' },
companyName: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
mobilePhone: { type: 'string' },
status: {
type: 'string',
enum: ['active', 'inactive'],
},
taxExempt: { type: 'boolean' },
},
required: ['customerId'],
},
handler: async (params: any) => {
const { customerId, ...updateData } = params;
const customer = await client.patch<Customer>(
`/customers/${customerId}`,
updateData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(customer, null, 2),
},
],
};
},
},
{
name: 'fieldedge_customers_delete',
description: 'Delete a customer (or mark as inactive)',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
},
required: ['customerId'],
},
handler: async (params: { customerId: string }) => {
await client.delete(`/customers/${params.customerId}`);
return {
content: [
{
type: 'text',
text: `Customer ${params.customerId} deleted successfully`,
},
],
};
},
},
{
name: 'fieldedge_customers_search',
description: 'Search customers by name, email, phone, or customer number',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
page: { type: 'number' },
pageSize: { type: 'number' },
},
required: ['query'],
},
handler: async (params: any) => {
const result = await client.getPaginated<Customer>('/customers/search', params as any);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_customers_locations_list',
description: 'List all locations for a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
},
required: ['customerId'],
},
handler: async (params: { customerId: string }) => {
const locations = await client.get<{ data: CustomerLocation[] }>(
`/customers/${params.customerId}/locations`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(locations, null, 2),
},
],
};
},
},
{
name: 'fieldedge_customers_equipment_list',
description: 'List all equipment for a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
},
required: ['customerId'],
},
handler: async (params: { customerId: string }) => {
const equipment = await client.get<{ data: Equipment[] }>(
`/customers/${params.customerId}/equipment`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(equipment, null, 2),
},
],
};
},
},
];
}

View File

@ -0,0 +1,237 @@
/**
* Customer Management Tools
*/
import { z } from 'zod';
import type { Customer, QueryParams } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
// Schemas
const AddressSchema = z.object({
street1: z.string(),
street2: z.string().optional(),
city: z.string(),
state: z.string(),
zip: z.string(),
country: z.string().optional(),
latitude: z.number().optional(),
longitude: z.number().optional(),
});
const CreateCustomerSchema = z.object({
firstName: z.string().describe('Customer first name'),
lastName: z.string().describe('Customer last name'),
companyName: z.string().optional().describe('Company name for commercial customers'),
email: z.string().email().optional().describe('Email address'),
phone: z.string().optional().describe('Primary phone number'),
mobilePhone: z.string().optional().describe('Mobile phone number'),
address: AddressSchema.optional().describe('Service address'),
billingAddress: AddressSchema.optional().describe('Billing address'),
customerType: z.enum(['residential', 'commercial']).describe('Customer type'),
taxExempt: z.boolean().default(false).describe('Tax exempt status'),
creditLimit: z.number().optional().describe('Credit limit'),
notes: z.string().optional().describe('Customer notes'),
tags: z.array(z.string()).optional().describe('Customer tags'),
customFields: z.record(z.any()).optional().describe('Custom field values'),
});
const UpdateCustomerSchema = z.object({
id: z.string().describe('Customer ID'),
firstName: z.string().optional(),
lastName: z.string().optional(),
companyName: z.string().optional(),
email: z.string().email().optional(),
phone: z.string().optional(),
mobilePhone: z.string().optional(),
address: AddressSchema.optional(),
billingAddress: AddressSchema.optional(),
status: z.enum(['active', 'inactive', 'prospect']).optional(),
customerType: z.enum(['residential', 'commercial']).optional(),
taxExempt: z.boolean().optional(),
creditLimit: z.number().optional(),
notes: z.string().optional(),
tags: z.array(z.string()).optional(),
customFields: z.record(z.any()).optional(),
});
const SearchCustomersSchema = z.object({
search: z.string().optional().describe('Search query'),
status: z.enum(['active', 'inactive', 'prospect']).optional(),
customerType: z.enum(['residential', 'commercial']).optional(),
tags: z.array(z.string()).optional(),
page: z.number().default(1),
pageSize: z.number().default(50),
sortBy: z.string().optional(),
sortOrder: z.enum(['asc', 'desc']).optional(),
});
// Tool Definitions
export const customerTools = [
{
name: 'fieldedge_list_customers',
description: 'List all customers with optional filtering and pagination',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Items per page (default: 50)' },
status: { type: 'string', enum: ['active', 'inactive', 'prospect'], description: 'Filter by status' },
customerType: { type: 'string', enum: ['residential', 'commercial'], description: 'Filter by type' },
sortBy: { type: 'string', description: 'Field to sort by' },
sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'Sort order' },
},
},
},
{
name: 'fieldedge_get_customer',
description: 'Get a specific customer by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Customer ID' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_customer',
description: 'Create a new customer',
inputSchema: zodToJsonSchema(CreateCustomerSchema),
},
{
name: 'fieldedge_update_customer',
description: 'Update an existing customer',
inputSchema: zodToJsonSchema(UpdateCustomerSchema),
},
{
name: 'fieldedge_delete_customer',
description: 'Delete a customer',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Customer ID' },
},
required: ['id'],
},
},
{
name: 'fieldedge_search_customers',
description: 'Search customers by name, email, phone, or other criteria',
inputSchema: zodToJsonSchema(SearchCustomersSchema),
},
{
name: 'fieldedge_get_customer_balance',
description: 'Get customer account balance and payment history',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Customer ID' },
},
required: ['id'],
},
},
{
name: 'fieldedge_get_customer_jobs',
description: 'Get all jobs for a specific customer',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Customer ID' },
status: { type: 'string', description: 'Filter by job status' },
page: { type: 'number' },
pageSize: { type: 'number' },
},
required: ['id'],
},
},
{
name: 'fieldedge_get_customer_invoices',
description: 'Get all invoices for a specific customer',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Customer ID' },
status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'partial', 'paid', 'overdue', 'void'] },
page: { type: 'number' },
pageSize: { type: 'number' },
},
required: ['id'],
},
},
{
name: 'fieldedge_get_customer_equipment',
description: 'Get all equipment for a specific customer',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Customer ID' },
status: { type: 'string', enum: ['active', 'inactive', 'decommissioned'] },
},
required: ['id'],
},
},
];
// Tool Handlers
export async function handleCustomerTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_customers':
return await client.getPaginated<Customer>('/customers', args);
case 'fieldedge_get_customer':
return await client.get<Customer>(`/customers/${args.id}`);
case 'fieldedge_create_customer':
const createData = CreateCustomerSchema.parse(args);
return await client.post<Customer>('/customers', createData);
case 'fieldedge_update_customer':
const updateData = UpdateCustomerSchema.parse(args);
const { id, ...updates } = updateData;
return await client.patch<Customer>(`/customers/${id}`, updates);
case 'fieldedge_delete_customer':
return await client.delete(`/customers/${args.id}`);
case 'fieldedge_search_customers':
const searchParams = SearchCustomersSchema.parse(args);
return await client.getPaginated<Customer>('/customers/search', searchParams);
case 'fieldedge_get_customer_balance':
return await client.get(`/customers/${args.id}/balance`);
case 'fieldedge_get_customer_jobs':
return await client.getPaginated(`/customers/${args.id}/jobs`, {
status: args.status,
page: args.page,
pageSize: args.pageSize,
});
case 'fieldedge_get_customer_invoices':
return await client.getPaginated(`/customers/${args.id}/invoices`, {
status: args.status,
page: args.page,
pageSize: args.pageSize,
});
case 'fieldedge_get_customer_equipment':
return await client.get(`/customers/${args.id}/equipment`, {
status: args.status,
});
default:
throw new Error(`Unknown customer tool: ${name}`);
}
}
// Helper function to convert Zod schema to JSON Schema
function zodToJsonSchema(schema: z.ZodType<any>): any {
// Simplified conversion - in production use zod-to-json-schema library
return {
type: 'object',
properties: {},
required: [],
};
}

View File

@ -1,164 +0,0 @@
/**
* FieldEdge Dispatch Tools
*/
import { FieldEdgeClient } from '../client.js';
import { DispatchBoard, TechnicianAvailability, DispatchZone } from '../types.js';
export function createDispatchTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_dispatch_board_get',
description: 'Get the dispatch board for a specific date',
inputSchema: {
type: 'object',
properties: {
date: {
type: 'string',
description: 'Date for dispatch board (ISO 8601 date)',
},
zoneId: {
type: 'string',
description: 'Filter by specific dispatch zone',
},
},
required: ['date'],
},
handler: async (params: { date: string; zoneId?: string }) => {
const board = await client.get<DispatchBoard>('/dispatch/board', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(board, null, 2),
},
],
};
},
},
{
name: 'fieldedge_dispatch_assign_tech',
description: 'Assign a technician to a job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string', description: 'Job ID' },
technicianId: { type: 'string', description: 'Technician ID' },
scheduledStart: {
type: 'string',
description: 'Scheduled start time (ISO 8601)',
},
scheduledEnd: {
type: 'string',
description: 'Scheduled end time (ISO 8601)',
},
notify: {
type: 'boolean',
description: 'Notify technician of assignment',
},
},
required: ['jobId', 'technicianId'],
},
handler: async (params: {
jobId: string;
technicianId: string;
scheduledStart?: string;
scheduledEnd?: string;
notify?: boolean;
}) => {
const result = await client.post<{ success: boolean; job: unknown }>(
'/dispatch/assign',
params
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_dispatch_technician_availability_get',
description: 'Get availability for a technician on a specific date',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'string', description: 'Technician ID' },
date: { type: 'string', description: 'Date (ISO 8601)' },
},
required: ['technicianId', 'date'],
},
handler: async (params: { technicianId: string; date: string }) => {
const availability = await client.get<TechnicianAvailability>(
`/dispatch/technicians/${params.technicianId}/availability`,
{ date: params.date }
);
return {
content: [
{
type: 'text',
text: JSON.stringify(availability, null, 2),
},
],
};
},
},
{
name: 'fieldedge_dispatch_zones_list',
description: 'List all dispatch zones',
inputSchema: {
type: 'object',
properties: {},
},
handler: async () => {
const zones = await client.get<{ data: DispatchZone[] }>('/dispatch/zones');
return {
content: [
{
type: 'text',
text: JSON.stringify(zones, null, 2),
},
],
};
},
},
{
name: 'fieldedge_dispatch_optimize',
description: 'Optimize dispatch schedule for a date (auto-assign based on location, skills, availability)',
inputSchema: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date to optimize (ISO 8601)' },
zoneId: { type: 'string', description: 'Limit to specific zone' },
preview: {
type: 'boolean',
description: 'Preview changes without applying',
},
},
required: ['date'],
},
handler: async (params: {
date: string;
zoneId?: string;
preview?: boolean;
}) => {
const result = await client.post<{
success: boolean;
changes: unknown[];
preview: boolean;
}>('/dispatch/optimize', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];
}

View File

@ -1,198 +0,0 @@
/**
* FieldEdge Equipment Tools
*/
import { FieldEdgeClient } from '../client.js';
import { Equipment, ServiceHistory, PaginationParams } from '../types.js';
export function createEquipmentTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_equipment_list',
description: 'List all equipment with optional filtering',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Filter by customer ID' },
locationId: { type: 'string', description: 'Filter by location ID' },
type: { type: 'string', description: 'Filter by equipment type' },
status: {
type: 'string',
description: 'Filter by status',
enum: ['active', 'inactive', 'retired'],
},
page: { type: 'number' },
pageSize: { type: 'number' },
},
},
handler: async (params: PaginationParams & {
customerId?: string;
locationId?: string;
type?: string;
status?: string;
}) => {
const result = await client.getPaginated<Equipment>('/equipment', params as any);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_equipment_get',
description: 'Get detailed information about specific equipment',
inputSchema: {
type: 'object',
properties: {
equipmentId: { type: 'string', description: 'Equipment ID' },
},
required: ['equipmentId'],
},
handler: async (params: { equipmentId: string }) => {
const equipment = await client.get<Equipment>(
`/equipment/${params.equipmentId}`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(equipment, null, 2),
},
],
};
},
},
{
name: 'fieldedge_equipment_create',
description: 'Create a new equipment record',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
locationId: { type: 'string', description: 'Location ID' },
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 date (ISO 8601)',
},
notes: { type: 'string', description: 'Equipment notes' },
},
required: ['customerId', 'type'],
},
handler: async (params: {
customerId: string;
locationId?: string;
type: string;
manufacturer?: string;
model?: string;
serialNumber?: string;
installDate?: string;
warrantyExpiration?: string;
notes?: string;
}) => {
const equipment = await client.post<Equipment>('/equipment', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(equipment, null, 2),
},
],
};
},
},
{
name: 'fieldedge_equipment_update',
description: 'Update an existing equipment record',
inputSchema: {
type: 'object',
properties: {
equipmentId: { type: 'string', description: 'Equipment ID' },
type: { type: 'string' },
manufacturer: { type: 'string' },
model: { type: 'string' },
serialNumber: { type: 'string' },
status: {
type: 'string',
enum: ['active', 'inactive', 'retired'],
},
installDate: { type: 'string' },
warrantyExpiration: { type: 'string' },
lastServiceDate: { type: 'string' },
nextServiceDate: { type: 'string' },
notes: { type: 'string' },
},
required: ['equipmentId'],
},
handler: async (params: {
equipmentId: string;
type?: string;
manufacturer?: string;
model?: string;
serialNumber?: string;
status?: string;
installDate?: string;
warrantyExpiration?: string;
lastServiceDate?: string;
nextServiceDate?: string;
notes?: string;
}) => {
const { equipmentId, ...updateData } = params;
const equipment = await client.patch<Equipment>(
`/equipment/${equipmentId}`,
updateData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(equipment, null, 2),
},
],
};
},
},
{
name: 'fieldedge_equipment_service_history_list',
description: 'List service history for equipment',
inputSchema: {
type: 'object',
properties: {
equipmentId: { type: 'string', description: 'Equipment ID' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
page: { type: 'number' },
pageSize: { type: 'number' },
},
required: ['equipmentId'],
},
handler: async (params: PaginationParams & {
equipmentId: string;
startDate?: string;
endDate?: string;
}) => {
const { equipmentId, ...queryParams } = params;
const result = await client.getPaginated<ServiceHistory>(
`/equipment/${equipmentId}/service-history`,
queryParams
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];
}

View File

@ -0,0 +1,158 @@
/**
* Equipment Management Tools
*/
import type { Equipment, ServiceHistory } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const equipmentTools = [
{
name: 'fieldedge_list_equipment',
description: 'List all equipment with optional filtering',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
customerId: { type: 'string' },
locationId: { type: 'string' },
status: { type: 'string', enum: ['active', 'inactive', 'decommissioned'] },
type: { type: 'string' },
manufacturer: { type: 'string' },
},
},
},
{
name: 'fieldedge_get_equipment',
description: 'Get specific equipment by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_equipment',
description: 'Create a new equipment record',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string' },
locationId: { type: 'string' },
type: { type: 'string' },
manufacturer: { type: 'string' },
model: { type: 'string' },
serialNumber: { type: 'string' },
installDate: { type: 'string' },
warrantyExpiry: { type: 'string' },
notes: { type: 'string' },
customFields: { type: 'object' },
},
required: ['customerId', 'type', 'manufacturer', 'model'],
},
},
{
name: 'fieldedge_update_equipment',
description: 'Update equipment record',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
status: { type: 'string', enum: ['active', 'inactive', 'decommissioned'] },
type: { type: 'string' },
manufacturer: { type: 'string' },
model: { type: 'string' },
serialNumber: { type: 'string' },
installDate: { type: 'string' },
warrantyExpiry: { type: 'string' },
lastServiceDate: { type: 'string' },
nextServiceDue: { type: 'string' },
notes: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_delete_equipment',
description: 'Delete equipment record',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_get_equipment_service_history',
description: 'Get service history for equipment',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_schedule_equipment_maintenance',
description: 'Schedule preventive maintenance for equipment',
inputSchema: {
type: 'object',
properties: {
equipmentId: { type: 'string' },
scheduledDate: { type: 'string' },
technicianId: { type: 'string' },
maintenanceType: { type: 'string' },
notes: { type: 'string' },
},
required: ['equipmentId', 'scheduledDate', 'maintenanceType'],
},
},
];
export async function handleEquipmentTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_equipment':
return await client.getPaginated<Equipment>('/equipment', args);
case 'fieldedge_get_equipment':
return await client.get<Equipment>(`/equipment/${args.id}`);
case 'fieldedge_create_equipment':
return await client.post<Equipment>('/equipment', args);
case 'fieldedge_update_equipment':
const { id, ...updates } = args;
return await client.patch<Equipment>(`/equipment/${id}`, updates);
case 'fieldedge_delete_equipment':
return await client.delete(`/equipment/${args.id}`);
case 'fieldedge_get_equipment_service_history':
return await client.get<ServiceHistory[]>(`/equipment/${args.id}/service-history`, {
startDate: args.startDate,
endDate: args.endDate,
});
case 'fieldedge_schedule_equipment_maintenance':
return await client.post('/jobs', {
customerId: args.customerId,
equipmentIds: [args.equipmentId],
jobType: 'maintenance',
description: args.maintenanceType,
scheduledStart: args.scheduledDate,
assignedTechnicians: args.technicianId ? [args.technicianId] : [],
notes: args.notes,
});
default:
throw new Error(`Unknown equipment tool: ${name}`);
}
}

View File

@ -1,215 +0,0 @@
/**
* FieldEdge Estimates Tools
*/
import { FieldEdgeClient } from '../client.js';
import { Estimate, PaginationParams } from '../types.js';
export function createEstimatesTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_estimates_list',
description: 'List all estimates with optional filtering',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
description: 'Filter by estimate status',
enum: ['draft', 'sent', 'approved', 'declined', 'expired'],
},
customerId: { type: 'string', description: 'Filter by customer ID' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
page: { type: 'number' },
pageSize: { type: 'number' },
},
},
handler: async (params: PaginationParams & {
status?: string;
customerId?: string;
startDate?: string;
endDate?: string;
}) => {
const result = await client.getPaginated<Estimate>('/estimates', params as any);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_estimates_get',
description: 'Get detailed information about a specific estimate',
inputSchema: {
type: 'object',
properties: {
estimateId: { type: 'string', description: 'Estimate ID' },
},
required: ['estimateId'],
},
handler: async (params: { estimateId: string }) => {
const estimate = await client.get<Estimate>(
`/estimates/${params.estimateId}`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(estimate, null, 2),
},
],
};
},
},
{
name: 'fieldedge_estimates_create',
description: 'Create a new estimate',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
locationId: { type: 'string', description: 'Customer location ID' },
estimateDate: { type: 'string', description: 'Estimate date (ISO 8601)' },
expirationDate: { type: 'string', description: 'Expiration date (ISO 8601)' },
terms: { type: 'string', description: 'Terms and conditions' },
notes: { type: 'string', description: 'Estimate notes' },
},
required: ['customerId'],
},
handler: async (params: {
customerId: string;
locationId?: string;
estimateDate?: string;
expirationDate?: string;
terms?: string;
notes?: string;
}) => {
const estimate = await client.post<Estimate>('/estimates', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(estimate, null, 2),
},
],
};
},
},
{
name: 'fieldedge_estimates_update',
description: 'Update an existing estimate',
inputSchema: {
type: 'object',
properties: {
estimateId: { type: 'string', description: 'Estimate ID' },
status: {
type: 'string',
enum: ['draft', 'sent', 'approved', 'declined', 'expired'],
},
expirationDate: { type: 'string' },
terms: { type: 'string' },
notes: { type: 'string' },
},
required: ['estimateId'],
},
handler: async (params: {
estimateId: string;
status?: string;
expirationDate?: string;
terms?: string;
notes?: string;
}) => {
const { estimateId, ...updateData } = params;
const estimate = await client.patch<Estimate>(
`/estimates/${estimateId}`,
updateData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(estimate, null, 2),
},
],
};
},
},
{
name: 'fieldedge_estimates_send',
description: 'Send an estimate to the customer',
inputSchema: {
type: 'object',
properties: {
estimateId: { type: 'string', description: 'Estimate ID' },
email: { type: 'string', description: 'Email address to send to' },
subject: { type: 'string', description: 'Email subject' },
message: { type: 'string', description: 'Email message' },
},
required: ['estimateId'],
},
handler: async (params: {
estimateId: string;
email?: string;
subject?: string;
message?: string;
}) => {
const { estimateId, ...sendData } = params;
const result = await client.post<{ success: boolean }>(
`/estimates/${estimateId}/send`,
sendData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_estimates_approve',
description: 'Approve an estimate and optionally convert to a job',
inputSchema: {
type: 'object',
properties: {
estimateId: { type: 'string', description: 'Estimate ID' },
convertToJob: {
type: 'boolean',
description: 'Convert to job automatically',
},
scheduledStart: {
type: 'string',
description: 'Scheduled start time if converting (ISO 8601)',
},
},
required: ['estimateId'],
},
handler: async (params: {
estimateId: string;
convertToJob?: boolean;
scheduledStart?: string;
}) => {
const { estimateId, ...approveData } = params;
const result = await client.post<{ estimate: Estimate; jobId?: string }>(
`/estimates/${estimateId}/approve`,
approveData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];
}

View File

@ -0,0 +1,173 @@
/**
* Estimate/Quote Management Tools
*/
import type { Estimate, EstimateStatus } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const estimateTools = [
{
name: 'fieldedge_list_estimates',
description: 'List all estimates with optional filtering',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'approved', 'declined', 'expired'] },
customerId: { type: 'string' },
sortBy: { type: 'string' },
sortOrder: { type: 'string', enum: ['asc', 'desc'] },
},
},
},
{
name: 'fieldedge_get_estimate',
description: 'Get a specific estimate by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Estimate ID' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_estimate',
description: 'Create a new estimate/quote',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string' },
issueDate: { type: 'string' },
expiryDate: { type: 'string' },
lineItems: {
type: 'array',
items: {
type: 'object',
properties: {
type: { type: 'string', enum: ['service', 'part', 'equipment', 'labor'] },
description: { type: 'string' },
quantity: { type: 'number' },
unitPrice: { type: 'number' },
discount: { type: 'number' },
tax: { type: 'number' },
},
},
},
notes: { type: 'string' },
},
required: ['customerId', 'issueDate', 'expiryDate', 'lineItems'],
},
},
{
name: 'fieldedge_update_estimate',
description: 'Update an existing estimate',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'approved', 'declined', 'expired'] },
expiryDate: { type: 'string' },
lineItems: { type: 'array' },
notes: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_delete_estimate',
description: 'Delete an estimate',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_send_estimate',
description: 'Send estimate to customer via email',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
subject: { type: 'string' },
message: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_approve_estimate',
description: 'Mark estimate as approved and optionally create a job',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
createJob: { type: 'boolean', default: true },
notes: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_convert_estimate_to_invoice',
description: 'Convert an approved estimate to an invoice',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
issueDate: { type: 'string' },
dueDate: { type: 'string' },
},
required: ['id'],
},
},
];
export async function handleEstimateTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_estimates':
return await client.getPaginated<Estimate>('/estimates', args);
case 'fieldedge_get_estimate':
return await client.get<Estimate>(`/estimates/${args.id}`);
case 'fieldedge_create_estimate':
return await client.post<Estimate>('/estimates', args);
case 'fieldedge_update_estimate':
const { id, ...updates } = args;
return await client.patch<Estimate>(`/estimates/${id}`, updates);
case 'fieldedge_delete_estimate':
return await client.delete(`/estimates/${args.id}`);
case 'fieldedge_send_estimate':
return await client.post(`/estimates/${args.id}/send`, {
email: args.email,
subject: args.subject,
message: args.message,
});
case 'fieldedge_approve_estimate':
return await client.post(`/estimates/${args.id}/approve`, {
createJob: args.createJob,
notes: args.notes,
});
case 'fieldedge_convert_estimate_to_invoice':
return await client.post(`/estimates/${args.id}/convert-to-invoice`, {
issueDate: args.issueDate,
dueDate: args.dueDate,
});
default:
throw new Error(`Unknown estimate tool: ${name}`);
}
}

View File

@ -1,207 +0,0 @@
/**
* FieldEdge Inventory Tools
*/
import { FieldEdgeClient } from '../client.js';
import { InventoryPart, PurchaseOrder, PaginationParams } from '../types.js';
export function createInventoryTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_inventory_parts_list',
description: 'List all inventory parts with optional filtering',
inputSchema: {
type: 'object',
properties: {
category: { type: 'string', description: 'Filter by category' },
manufacturer: { type: 'string', description: 'Filter by manufacturer' },
lowStock: {
type: 'boolean',
description: 'Show only low stock items',
},
search: { type: 'string', description: 'Search by part number or description' },
page: { type: 'number' },
pageSize: { type: 'number' },
},
},
handler: async (params: {
category?: string;
manufacturer?: string;
lowStock?: boolean;
search?: string;
}) => {
const result = await client.getPaginated<InventoryPart>(
'/inventory/parts',
params
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_inventory_parts_get',
description: 'Get detailed information about a specific part',
inputSchema: {
type: 'object',
properties: {
partId: { type: 'string', description: 'Part ID' },
},
required: ['partId'],
},
handler: async (params: { partId: string }) => {
const part = await client.get<InventoryPart>(
`/inventory/parts/${params.partId}`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(part, null, 2),
},
],
};
},
},
{
name: 'fieldedge_inventory_stock_update',
description: 'Update stock quantity for a part',
inputSchema: {
type: 'object',
properties: {
partId: { type: 'string', description: 'Part ID' },
quantityChange: {
type: 'number',
description: 'Quantity change (positive for add, negative for subtract)',
},
reason: {
type: 'string',
description: 'Reason for stock adjustment',
enum: [
'purchase',
'return',
'adjustment',
'damage',
'theft',
'transfer',
'cycle_count',
],
},
notes: { type: 'string', description: 'Adjustment notes' },
},
required: ['partId', 'quantityChange', 'reason'],
},
handler: async (params: {
partId: string;
quantityChange: number;
reason: string;
notes?: string;
}) => {
const { partId, ...adjustmentData } = params;
const result = await client.post<InventoryPart>(
`/inventory/parts/${partId}/adjust`,
adjustmentData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_inventory_purchase_orders_list',
description: 'List all purchase orders',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
description: 'Filter by PO status',
enum: ['draft', 'submitted', 'approved', 'received', 'cancelled'],
},
vendorId: { type: 'string', description: 'Filter by vendor ID' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
page: { type: 'number' },
pageSize: { type: 'number' },
},
},
handler: async (params: {
status?: string;
vendorId?: string;
startDate?: string;
endDate?: string;
}) => {
const result = await client.getPaginated<PurchaseOrder>(
'/inventory/purchase-orders',
params
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_inventory_purchase_orders_get',
description: 'Get detailed information about a purchase order',
inputSchema: {
type: 'object',
properties: {
poId: { type: 'string', description: 'Purchase Order ID' },
},
required: ['poId'],
},
handler: async (params: { poId: string }) => {
const po = await client.get<PurchaseOrder>(
`/inventory/purchase-orders/${params.poId}`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(po, null, 2),
},
],
};
},
},
{
name: 'fieldedge_inventory_reorder_report',
description: 'Get a report of parts that need reordering',
inputSchema: {
type: 'object',
properties: {
category: { type: 'string', description: 'Filter by category' },
},
},
handler: async (params: { category?: string }) => {
const report = await client.get<{ data: InventoryPart[] }>(
'/inventory/reorder-report',
params
);
return {
content: [
{
type: 'text',
text: JSON.stringify(report, null, 2),
},
],
};
},
},
];
}

View File

@ -0,0 +1,147 @@
/**
* Inventory Management Tools
*/
import type { InventoryItem, InventoryTransaction } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const inventoryTools = [
{
name: 'fieldedge_list_inventory',
description: 'List inventory items',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
category: { type: 'string' },
manufacturer: { type: 'string' },
warehouse: { type: 'string' },
lowStock: { type: 'boolean', description: 'Show only low stock items' },
search: { type: 'string' },
},
},
},
{
name: 'fieldedge_get_inventory_item',
description: 'Get specific inventory item',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_inventory_item',
description: 'Create new inventory item',
inputSchema: {
type: 'object',
properties: {
sku: { type: 'string' },
name: { type: 'string' },
description: { type: 'string' },
category: { type: 'string' },
manufacturer: { type: 'string' },
modelNumber: { type: 'string' },
unitOfMeasure: { type: 'string' },
costPrice: { type: 'number' },
sellPrice: { type: 'number' },
reorderPoint: { type: 'number' },
reorderQuantity: { type: 'number' },
warehouse: { type: 'string' },
binLocation: { type: 'string' },
taxable: { type: 'boolean' },
},
required: ['sku', 'name', 'category', 'unitOfMeasure', 'costPrice', 'sellPrice'],
},
},
{
name: 'fieldedge_update_inventory_item',
description: 'Update inventory item',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
description: { type: 'string' },
category: { type: 'string' },
costPrice: { type: 'number' },
sellPrice: { type: 'number' },
reorderPoint: { type: 'number' },
reorderQuantity: { type: 'number' },
},
required: ['id'],
},
},
{
name: 'fieldedge_adjust_inventory',
description: 'Adjust inventory quantity',
inputSchema: {
type: 'object',
properties: {
itemId: { type: 'string' },
quantity: { type: 'number', description: 'Adjustment quantity (positive or negative)' },
type: { type: 'string', enum: ['receipt', 'issue', 'adjustment', 'transfer', 'return'] },
reference: { type: 'string' },
notes: { type: 'string' },
},
required: ['itemId', 'quantity', 'type'],
},
},
{
name: 'fieldedge_get_inventory_transactions',
description: 'Get inventory transaction history',
inputSchema: {
type: 'object',
properties: {
itemId: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
type: { type: 'string', enum: ['receipt', 'issue', 'adjustment', 'transfer', 'return'] },
},
},
},
{
name: 'fieldedge_get_low_stock_items',
description: 'Get items below reorder point',
inputSchema: {
type: 'object',
properties: {
warehouse: { type: 'string' },
},
},
},
];
export async function handleInventoryTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_inventory':
return await client.getPaginated<InventoryItem>('/inventory', args);
case 'fieldedge_get_inventory_item':
return await client.get<InventoryItem>(`/inventory/${args.id}`);
case 'fieldedge_create_inventory_item':
return await client.post<InventoryItem>('/inventory', args);
case 'fieldedge_update_inventory_item':
const { id, ...updates } = args;
return await client.patch<InventoryItem>(`/inventory/${id}`, updates);
case 'fieldedge_adjust_inventory':
return await client.post<InventoryTransaction>('/inventory/transactions', args);
case 'fieldedge_get_inventory_transactions':
return await client.getPaginated<InventoryTransaction>('/inventory/transactions', args);
case 'fieldedge_get_low_stock_items':
return await client.get<InventoryItem[]>('/inventory/low-stock', args);
default:
throw new Error(`Unknown inventory tool: ${name}`);
}
}

View File

@ -1,207 +0,0 @@
/**
* FieldEdge Invoices Tools
*/
import { FieldEdgeClient } from '../client.js';
import { Invoice, Payment, PaginationParams } from '../types.js';
export function createInvoicesTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_invoices_list',
description: 'List all invoices with optional filtering',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
description: 'Filter by invoice status',
enum: ['draft', 'sent', 'paid', 'partial', 'overdue', 'void'],
},
customerId: { type: 'string', description: 'Filter by customer ID' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
page: { type: 'number' },
pageSize: { type: 'number' },
},
},
handler: async (params: PaginationParams & {
status?: string;
customerId?: string;
startDate?: string;
endDate?: string;
}) => {
const result = await client.getPaginated<Invoice>('/invoices', params as any);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_invoices_get',
description: 'Get detailed information about a specific invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
handler: async (params: { invoiceId: string }) => {
const invoice = await client.get<Invoice>(`/invoices/${params.invoiceId}`);
return {
content: [
{
type: 'text',
text: JSON.stringify(invoice, null, 2),
},
],
};
},
},
{
name: 'fieldedge_invoices_create',
description: 'Create a new invoice',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
jobId: { type: 'string', description: 'Related job ID' },
invoiceDate: { type: 'string', description: 'Invoice date (ISO 8601)' },
dueDate: { type: 'string', description: 'Due date (ISO 8601)' },
terms: { type: 'string', description: 'Payment terms' },
notes: { type: 'string', description: 'Invoice notes' },
},
required: ['customerId'],
},
handler: async (params: {
customerId: string;
jobId?: string;
invoiceDate?: string;
dueDate?: string;
terms?: string;
notes?: string;
}) => {
const invoice = await client.post<Invoice>('/invoices', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(invoice, null, 2),
},
],
};
},
},
{
name: 'fieldedge_invoices_update',
description: 'Update an existing invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string', description: 'Invoice ID' },
status: {
type: 'string',
enum: ['draft', 'sent', 'paid', 'partial', 'overdue', 'void'],
},
dueDate: { type: 'string' },
terms: { type: 'string' },
notes: { type: 'string' },
},
required: ['invoiceId'],
},
handler: async (params: {
invoiceId: string;
status?: string;
dueDate?: string;
terms?: string;
notes?: string;
}) => {
const { invoiceId, ...updateData } = params;
const invoice = await client.patch<Invoice>(
`/invoices/${invoiceId}`,
updateData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(invoice, null, 2),
},
],
};
},
},
{
name: 'fieldedge_invoices_payments_list',
description: 'List all payments for an invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
handler: async (params: { invoiceId: string }) => {
const payments = await client.get<{ data: Payment[] }>(
`/invoices/${params.invoiceId}/payments`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(payments, null, 2),
},
],
};
},
},
{
name: 'fieldedge_invoices_payments_add',
description: 'Add a payment to an invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string', description: 'Invoice ID' },
amount: { type: 'number', description: 'Payment amount' },
paymentMethod: {
type: 'string',
description: 'Payment method',
enum: ['cash', 'check', 'credit_card', 'ach', 'other'],
},
paymentDate: { type: 'string', description: 'Payment date (ISO 8601)' },
referenceNumber: { type: 'string', description: 'Reference/check number' },
notes: { type: 'string', description: 'Payment notes' },
},
required: ['invoiceId', 'amount', 'paymentMethod'],
},
handler: async (params: {
invoiceId: string;
amount: number;
paymentMethod: string;
paymentDate?: string;
referenceNumber?: string;
notes?: string;
}) => {
const { invoiceId, ...paymentData } = params;
const payment = await client.post<Payment>(
`/invoices/${invoiceId}/payments`,
paymentData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(payment, null, 2),
},
],
};
},
},
];
}

View File

@ -0,0 +1,201 @@
/**
* Invoice Management Tools
*/
import { z } from 'zod';
import type { Invoice, InvoiceStatus, LineItem } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const invoiceTools = [
{
name: 'fieldedge_list_invoices',
description: 'List all invoices with optional filtering and pagination',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'partial', 'paid', 'overdue', 'void'] },
customerId: { type: 'string', description: 'Filter by customer ID' },
jobId: { type: 'string', description: 'Filter by job ID' },
startDate: { type: 'string', description: 'Filter invoices from this date' },
endDate: { type: 'string', description: 'Filter invoices to this date' },
sortBy: { type: 'string' },
sortOrder: { type: 'string', enum: ['asc', 'desc'] },
},
},
},
{
name: 'fieldedge_get_invoice',
description: 'Get a specific invoice by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Invoice ID' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_invoice',
description: 'Create a new invoice',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
jobId: { type: 'string', description: 'Associated job ID' },
issueDate: { type: 'string', description: 'Issue date (ISO 8601)' },
dueDate: { type: 'string', description: 'Due date (ISO 8601)' },
lineItems: {
type: 'array',
items: {
type: 'object',
properties: {
type: { type: 'string', enum: ['service', 'part', 'equipment', 'labor'] },
description: { type: 'string' },
quantity: { type: 'number' },
unitPrice: { type: 'number' },
discount: { type: 'number', default: 0 },
tax: { type: 'number', default: 0 },
itemId: { type: 'string' },
},
required: ['type', 'description', 'quantity', 'unitPrice'],
},
},
paymentTerms: { type: 'string' },
notes: { type: 'string' },
},
required: ['customerId', 'issueDate', 'dueDate', 'lineItems'],
},
},
{
name: 'fieldedge_update_invoice',
description: 'Update an existing invoice',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Invoice ID' },
status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'partial', 'paid', 'overdue', 'void'] },
dueDate: { type: 'string' },
lineItems: { type: 'array' },
discount: { type: 'number' },
paymentTerms: { type: 'string' },
notes: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_delete_invoice',
description: 'Delete an invoice',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Invoice ID' },
},
required: ['id'],
},
},
{
name: 'fieldedge_send_invoice',
description: 'Send invoice to customer via email',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Invoice ID' },
email: { type: 'string', description: 'Override customer email' },
subject: { type: 'string', description: 'Email subject' },
message: { type: 'string', description: 'Email message' },
},
required: ['id'],
},
},
{
name: 'fieldedge_void_invoice',
description: 'Void an invoice',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Invoice ID' },
reason: { type: 'string', description: 'Reason for voiding' },
},
required: ['id'],
},
},
{
name: 'fieldedge_record_payment',
description: 'Record a payment against an invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string', description: 'Invoice ID' },
amount: { type: 'number', description: 'Payment amount' },
paymentMethod: { type: 'string', enum: ['cash', 'check', 'credit-card', 'debit-card', 'ach', 'wire', 'other'] },
paymentDate: { type: 'string', description: 'Payment date (ISO 8601)' },
reference: { type: 'string', description: 'Payment reference/confirmation number' },
notes: { type: 'string' },
},
required: ['invoiceId', 'amount', 'paymentMethod'],
},
},
{
name: 'fieldedge_get_invoice_pdf',
description: 'Generate and get invoice PDF',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Invoice ID' },
},
required: ['id'],
},
},
];
export async function handleInvoiceTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_invoices':
return await client.getPaginated<Invoice>('/invoices', args);
case 'fieldedge_get_invoice':
return await client.get<Invoice>(`/invoices/${args.id}`);
case 'fieldedge_create_invoice':
return await client.post<Invoice>('/invoices', args);
case 'fieldedge_update_invoice':
const { id, ...updates } = args;
return await client.patch<Invoice>(`/invoices/${id}`, updates);
case 'fieldedge_delete_invoice':
return await client.delete(`/invoices/${args.id}`);
case 'fieldedge_send_invoice':
return await client.post(`/invoices/${args.id}/send`, {
email: args.email,
subject: args.subject,
message: args.message,
});
case 'fieldedge_void_invoice':
return await client.post(`/invoices/${args.id}/void`, {
reason: args.reason,
});
case 'fieldedge_record_payment':
return await client.post('/payments', args);
case 'fieldedge_get_invoice_pdf':
const pdfData = await client.downloadFile(`/invoices/${args.id}/pdf`);
return {
success: true,
message: 'PDF generated successfully',
size: pdfData.length,
data: pdfData.toString('base64'),
};
default:
throw new Error(`Unknown invoice tool: ${name}`);
}
}

View File

@ -1,325 +0,0 @@
/**
* FieldEdge Jobs Tools
*/
import { FieldEdgeClient } from '../client.js';
import { Job, JobLineItem, JobEquipment, PaginationParams } from '../types.js';
export function createJobsTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_jobs_list',
description: 'List all jobs with optional filtering by status, customer, technician, or date range',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
description: 'Filter by job status',
enum: ['scheduled', 'in_progress', 'completed', 'cancelled', 'on_hold'],
},
customerId: {
type: 'string',
description: 'Filter by customer ID',
},
technicianId: {
type: 'string',
description: 'Filter by assigned technician ID',
},
startDate: {
type: 'string',
description: 'Filter jobs scheduled after this date (ISO 8601)',
},
endDate: {
type: 'string',
description: 'Filter jobs scheduled before this date (ISO 8601)',
},
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Items per page (default: 50)' },
},
},
handler: async (params: PaginationParams & {
status?: string;
customerId?: string;
technicianId?: string;
startDate?: string;
endDate?: string;
}) => {
const result = await client.getPaginated<Job>('/jobs', params as any);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_jobs_get',
description: 'Get detailed information about a specific job by ID',
inputSchema: {
type: 'object',
properties: {
jobId: {
type: 'string',
description: 'The job ID',
},
},
required: ['jobId'],
},
handler: async (params: { jobId: string }) => {
const job = await client.get<Job>(`/jobs/${params.jobId}`);
return {
content: [
{
type: 'text',
text: JSON.stringify(job, null, 2),
},
],
};
},
},
{
name: 'fieldedge_jobs_create',
description: 'Create a new job',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
locationId: { type: 'string', description: 'Customer location ID' },
jobType: { type: 'string', description: 'Job type' },
priority: {
type: 'string',
description: 'Job priority',
enum: ['low', 'normal', 'high', 'emergency'],
},
scheduledStart: {
type: 'string',
description: 'Scheduled start time (ISO 8601)',
},
scheduledEnd: {
type: 'string',
description: 'Scheduled end time (ISO 8601)',
},
assignedTechId: { type: 'string', description: 'Assigned technician ID' },
description: { type: 'string', description: 'Job description' },
notes: { type: 'string', description: 'Internal notes' },
},
required: ['customerId', 'jobType'],
},
handler: async (params: {
customerId: string;
locationId?: string;
jobType: string;
priority?: string;
scheduledStart?: string;
scheduledEnd?: string;
assignedTechId?: string;
description?: string;
notes?: string;
}) => {
const job = await client.post<Job>('/jobs', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(job, null, 2),
},
],
};
},
},
{
name: 'fieldedge_jobs_update',
description: 'Update an existing job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string', description: 'Job ID' },
status: {
type: 'string',
description: 'Job status',
enum: ['scheduled', 'in_progress', 'completed', 'cancelled', 'on_hold'],
},
priority: {
type: 'string',
enum: ['low', 'normal', 'high', 'emergency'],
},
assignedTechId: { type: 'string' },
scheduledStart: { type: 'string' },
scheduledEnd: { type: 'string' },
description: { type: 'string' },
notes: { type: 'string' },
},
required: ['jobId'],
},
handler: async (params: {
jobId: string;
status?: string;
priority?: string;
assignedTechId?: string;
scheduledStart?: string;
scheduledEnd?: string;
description?: string;
notes?: string;
}) => {
const { jobId, ...updateData } = params;
const job = await client.patch<Job>(`/jobs/${jobId}`, updateData);
return {
content: [
{
type: 'text',
text: JSON.stringify(job, null, 2),
},
],
};
},
},
{
name: 'fieldedge_jobs_complete',
description: 'Mark a job as completed',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string', description: 'Job ID' },
completionNotes: { type: 'string', description: 'Completion notes' },
},
required: ['jobId'],
},
handler: async (params: { jobId: string; completionNotes?: string }) => {
const job = await client.post<Job>(`/jobs/${params.jobId}/complete`, {
notes: params.completionNotes,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(job, null, 2),
},
],
};
},
},
{
name: 'fieldedge_jobs_cancel',
description: 'Cancel a job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string', description: 'Job ID' },
reason: { type: 'string', description: 'Cancellation reason' },
},
required: ['jobId'],
},
handler: async (params: { jobId: string; reason?: string }) => {
const job = await client.post<Job>(`/jobs/${params.jobId}/cancel`, {
reason: params.reason,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(job, null, 2),
},
],
};
},
},
{
name: 'fieldedge_jobs_line_items_list',
description: 'List all line items for a job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string', description: 'Job ID' },
},
required: ['jobId'],
},
handler: async (params: { jobId: string }) => {
const lineItems = await client.get<{ data: JobLineItem[] }>(
`/jobs/${params.jobId}/line-items`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(lineItems, null, 2),
},
],
};
},
},
{
name: 'fieldedge_jobs_line_items_add',
description: 'Add a line item to a job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string', description: 'Job ID' },
type: {
type: 'string',
description: 'Line item type',
enum: ['labor', 'material', 'equipment', 'other'],
},
description: { type: 'string', description: 'Item description' },
quantity: { type: 'number', description: 'Quantity' },
unitPrice: { type: 'number', description: 'Unit price' },
taxable: { type: 'boolean', description: 'Is taxable' },
partNumber: { type: 'string', description: 'Part number (for materials)' },
technicianId: { type: 'string', description: 'Technician ID (for labor)' },
},
required: ['jobId', 'type', 'description', 'quantity', 'unitPrice'],
},
handler: async (params: {
jobId: string;
type: string;
description: string;
quantity: number;
unitPrice: number;
taxable?: boolean;
partNumber?: string;
technicianId?: string;
}) => {
const { jobId, ...itemData } = params;
const lineItem = await client.post<JobLineItem>(
`/jobs/${jobId}/line-items`,
itemData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(lineItem, null, 2),
},
],
};
},
},
{
name: 'fieldedge_jobs_equipment_list',
description: 'List equipment associated with a job',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string', description: 'Job ID' },
},
required: ['jobId'],
},
handler: async (params: { jobId: string }) => {
const equipment = await client.get<{ data: JobEquipment[] }>(
`/jobs/${params.jobId}/equipment`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(equipment, null, 2),
},
],
};
},
},
];
}

View File

@ -0,0 +1,200 @@
/**
* Job Management Tools
*/
import { z } from 'zod';
import type { Job, JobStatus } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
// Tool Definitions
export const jobTools = [
{
name: 'fieldedge_list_jobs',
description: 'List all jobs with optional filtering and pagination',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number', description: 'Page number' },
pageSize: { type: 'number', description: 'Items per page' },
status: {
type: 'string',
enum: ['scheduled', 'dispatched', 'in-progress', 'on-hold', 'completed', 'cancelled', 'invoiced'],
description: 'Filter by status'
},
priority: { type: 'string', enum: ['low', 'normal', 'high', 'emergency'] },
customerId: { type: 'string', description: 'Filter by customer ID' },
technicianId: { type: 'string', description: 'Filter by technician ID' },
startDate: { type: 'string', description: 'Filter jobs starting from this date' },
endDate: { type: 'string', description: 'Filter jobs ending before this date' },
sortBy: { type: 'string' },
sortOrder: { type: 'string', enum: ['asc', 'desc'] },
},
},
},
{
name: 'fieldedge_get_job',
description: 'Get a specific job by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Job ID' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_job',
description: 'Create a new job/work order',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
locationId: { type: 'string', description: 'Service location ID' },
jobType: { type: 'string', description: 'Type of job' },
priority: { type: 'string', enum: ['low', 'normal', 'high', 'emergency'], default: 'normal' },
description: { type: 'string', description: 'Job description' },
scheduledStart: { type: 'string', description: 'Scheduled start date/time (ISO 8601)' },
scheduledEnd: { type: 'string', description: 'Scheduled end date/time (ISO 8601)' },
assignedTechnicians: { type: 'array', items: { type: 'string' }, description: 'Technician IDs' },
equipmentIds: { type: 'array', items: { type: 'string' }, description: 'Equipment IDs' },
tags: { type: 'array', items: { type: 'string' } },
customFields: { type: 'object' },
},
required: ['customerId', 'jobType', 'description'],
},
},
{
name: 'fieldedge_update_job',
description: 'Update an existing job',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Job ID' },
jobType: { type: 'string' },
status: { type: 'string', enum: ['scheduled', 'dispatched', 'in-progress', 'on-hold', 'completed', 'cancelled', 'invoiced'] },
priority: { type: 'string', enum: ['low', 'normal', 'high', 'emergency'] },
description: { type: 'string' },
scheduledStart: { type: 'string' },
scheduledEnd: { type: 'string' },
actualStart: { type: 'string' },
actualEnd: { type: 'string' },
assignedTechnicians: { type: 'array', items: { type: 'string' } },
equipmentIds: { type: 'array', items: { type: 'string' } },
tags: { type: 'array', items: { type: 'string' } },
customFields: { type: 'object' },
},
required: ['id'],
},
},
{
name: 'fieldedge_delete_job',
description: 'Delete a job',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Job ID' },
},
required: ['id'],
},
},
{
name: 'fieldedge_start_job',
description: 'Start a job (set status to in-progress and record actual start time)',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Job ID' },
notes: { type: 'string', description: 'Notes about starting the job' },
},
required: ['id'],
},
},
{
name: 'fieldedge_complete_job',
description: 'Complete a job (set status to completed and record actual end time)',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Job ID' },
notes: { type: 'string', description: 'Completion notes' },
createInvoice: { type: 'boolean', default: false, description: 'Automatically create invoice' },
},
required: ['id'],
},
},
{
name: 'fieldedge_cancel_job',
description: 'Cancel a job',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Job ID' },
reason: { type: 'string', description: 'Cancellation reason' },
},
required: ['id'],
},
},
{
name: 'fieldedge_assign_technician',
description: 'Assign or reassign technicians to a job',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Job ID' },
technicianIds: { type: 'array', items: { type: 'string' }, description: 'Technician IDs to assign' },
replace: { type: 'boolean', default: false, description: 'Replace existing technicians or add to them' },
},
required: ['id', 'technicianIds'],
},
},
];
// Tool Handlers
export async function handleJobTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_jobs':
return await client.getPaginated<Job>('/jobs', args);
case 'fieldedge_get_job':
return await client.get<Job>(`/jobs/${args.id}`);
case 'fieldedge_create_job':
return await client.post<Job>('/jobs', args);
case 'fieldedge_update_job':
const { id, ...updates } = args;
return await client.patch<Job>(`/jobs/${id}`, updates);
case 'fieldedge_delete_job':
return await client.delete(`/jobs/${args.id}`);
case 'fieldedge_start_job':
return await client.post<Job>(`/jobs/${args.id}/start`, {
actualStart: new Date().toISOString(),
notes: args.notes,
});
case 'fieldedge_complete_job':
return await client.post<Job>(`/jobs/${args.id}/complete`, {
actualEnd: new Date().toISOString(),
notes: args.notes,
createInvoice: args.createInvoice,
});
case 'fieldedge_cancel_job':
return await client.post<Job>(`/jobs/${args.id}/cancel`, {
reason: args.reason,
});
case 'fieldedge_assign_technician':
return await client.post<Job>(`/jobs/${args.id}/assign`, {
technicianIds: args.technicianIds,
replace: args.replace,
});
default:
throw new Error(`Unknown job tool: ${name}`);
}
}

View File

@ -0,0 +1,112 @@
/**
* Location Management Tools
*/
import type { Location } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const locationTools = [
{
name: 'fieldedge_list_locations',
description: 'List customer locations',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string' },
status: { type: 'string', enum: ['active', 'inactive'] },
type: { type: 'string', enum: ['primary', 'secondary', 'billing', 'service'] },
},
},
},
{
name: 'fieldedge_get_location',
description: 'Get specific location',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_location',
description: 'Create new location for customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string' },
name: { type: 'string' },
type: { type: 'string', enum: ['primary', 'secondary', 'billing', 'service'] },
address: {
type: 'object',
properties: {
street1: { type: 'string' },
street2: { type: 'string' },
city: { type: 'string' },
state: { type: 'string' },
zip: { type: 'string' },
},
},
contactName: { type: 'string' },
contactPhone: { type: 'string' },
accessNotes: { type: 'string' },
gateCode: { type: 'string' },
},
required: ['customerId', 'name', 'type', 'address'],
},
},
{
name: 'fieldedge_update_location',
description: 'Update location details',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
status: { type: 'string', enum: ['active', 'inactive'] },
contactName: { type: 'string' },
contactPhone: { type: 'string' },
accessNotes: { type: 'string' },
gateCode: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_delete_location',
description: 'Delete a location',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
];
export async function handleLocationTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_locations':
return await client.getPaginated<Location>('/locations', args);
case 'fieldedge_get_location':
return await client.get<Location>(`/locations/${args.id}`);
case 'fieldedge_create_location':
return await client.post<Location>('/locations', args);
case 'fieldedge_update_location':
const { id, ...updates } = args;
return await client.patch<Location>(`/locations/${id}`, updates);
case 'fieldedge_delete_location':
return await client.delete(`/locations/${args.id}`);
default:
throw new Error(`Unknown location tool: ${name}`);
}
}

View File

@ -0,0 +1,108 @@
/**
* Payment Management Tools
*/
import type { Payment } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const paymentTools = [
{
name: 'fieldedge_list_payments',
description: 'List all payments',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
customerId: { type: 'string' },
invoiceId: { type: 'string' },
status: { type: 'string', enum: ['pending', 'processed', 'failed', 'refunded'] },
paymentMethod: { type: 'string', enum: ['cash', 'check', 'credit-card', 'debit-card', 'ach', 'wire', 'other'] },
startDate: { type: 'string' },
endDate: { type: 'string' },
},
},
},
{
name: 'fieldedge_get_payment',
description: 'Get specific payment',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_process_payment',
description: 'Process a new payment',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string' },
customerId: { type: 'string' },
amount: { type: 'number' },
paymentMethod: { type: 'string', enum: ['cash', 'check', 'credit-card', 'debit-card', 'ach', 'wire', 'other'] },
paymentDate: { type: 'string' },
reference: { type: 'string' },
notes: { type: 'string' },
},
required: ['invoiceId', 'customerId', 'amount', 'paymentMethod'],
},
},
{
name: 'fieldedge_refund_payment',
description: 'Refund a payment',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
amount: { type: 'number' },
reason: { type: 'string' },
},
required: ['id', 'amount', 'reason'],
},
},
{
name: 'fieldedge_void_payment',
description: 'Void a payment',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
reason: { type: 'string' },
},
required: ['id', 'reason'],
},
},
];
export async function handlePaymentTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_payments':
return await client.getPaginated<Payment>('/payments', args);
case 'fieldedge_get_payment':
return await client.get<Payment>(`/payments/${args.id}`);
case 'fieldedge_process_payment':
return await client.post<Payment>('/payments', args);
case 'fieldedge_refund_payment':
return await client.post<Payment>(`/payments/${args.id}/refund`, {
amount: args.amount,
reason: args.reason,
});
case 'fieldedge_void_payment':
return await client.post(`/payments/${args.id}/void`, {
reason: args.reason,
});
default:
throw new Error(`Unknown payment tool: ${name}`);
}
}

View File

@ -1,237 +0,0 @@
/**
* FieldEdge Reporting Tools
*/
import { FieldEdgeClient } from '../client.js';
import {
RevenueReport,
JobProfitabilityReport,
TechnicianPerformance,
AgingReport,
} from '../types.js';
export function createReportingTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_reports_revenue',
description: 'Get revenue report for a specified period',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
groupBy: {
type: 'string',
description: 'Group results by dimension',
enum: ['day', 'week', 'month', 'jobType', 'technician', 'customer'],
},
},
required: ['startDate', 'endDate'],
},
handler: async (params: {
startDate: string;
endDate: string;
groupBy?: string;
}) => {
const report = await client.get<RevenueReport>('/reports/revenue', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(report, null, 2),
},
],
};
},
},
{
name: 'fieldedge_reports_job_profitability',
description: 'Get profitability analysis for a specific job or all jobs in a period',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string', description: 'Specific job ID (optional)' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
minMargin: {
type: 'number',
description: 'Filter jobs with profit margin above this percentage',
},
maxMargin: {
type: 'number',
description: 'Filter jobs with profit margin below this percentage',
},
},
},
handler: async (params: {
jobId?: string;
startDate?: string;
endDate?: string;
minMargin?: number;
maxMargin?: number;
}) => {
const report = await client.get<
JobProfitabilityReport | { data: JobProfitabilityReport[] }
>('/reports/job-profitability', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(report, null, 2),
},
],
};
},
},
{
name: 'fieldedge_reports_technician_performance',
description: 'Get performance metrics for all technicians or a specific technician',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'string', description: 'Specific technician ID (optional)' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
sortBy: {
type: 'string',
description: 'Sort results by metric',
enum: ['revenue', 'jobsCompleted', 'efficiency', 'customerSatisfaction'],
},
},
required: ['startDate', 'endDate'],
},
handler: async (params: {
technicianId?: string;
startDate: string;
endDate: string;
sortBy?: string;
}) => {
const report = await client.get<
TechnicianPerformance | { data: TechnicianPerformance[] }
>('/reports/technician-performance', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(report, null, 2),
},
],
};
},
},
{
name: 'fieldedge_reports_aging',
description: 'Get accounts receivable aging report',
inputSchema: {
type: 'object',
properties: {
asOfDate: {
type: 'string',
description: 'As-of date for the report (ISO 8601, defaults to today)',
},
customerId: {
type: 'string',
description: 'Filter by specific customer ID',
},
minAmount: {
type: 'number',
description: 'Show only customers with balance above this amount',
},
},
},
handler: async (params: {
asOfDate?: string;
customerId?: string;
minAmount?: number;
}) => {
const report = await client.get<AgingReport>('/reports/aging', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(report, null, 2),
},
],
};
},
},
{
name: 'fieldedge_reports_service_agreement_revenue',
description: 'Get revenue breakdown from service agreements/memberships',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
agreementType: { type: 'string', description: 'Filter by agreement type' },
},
required: ['startDate', 'endDate'],
},
handler: async (params: {
startDate: string;
endDate: string;
agreementType?: string;
}) => {
const report = await client.get<{
period: string;
totalRevenue: number;
activeAgreements: number;
newAgreements: number;
cancelledAgreements: number;
renewalRate: number;
byType: Record<string, number>;
}>('/reports/agreement-revenue', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(report, null, 2),
},
],
};
},
},
{
name: 'fieldedge_reports_equipment_service_due',
description: 'Get report of equipment due for service',
inputSchema: {
type: 'object',
properties: {
daysAhead: {
type: 'number',
description: 'Look ahead this many days (default: 30)',
},
customerId: { type: 'string', description: 'Filter by customer ID' },
equipmentType: { type: 'string', description: 'Filter by equipment type' },
},
},
handler: async (params: {
daysAhead?: number;
customerId?: string;
equipmentType?: string;
}) => {
const report = await client.get<{
data: Array<{
equipmentId: string;
customerId: string;
customerName: string;
equipmentType: string;
model: string;
lastServiceDate: string;
nextServiceDate: string;
daysUntilDue: number;
isOverdue: boolean;
}>;
}>('/reports/equipment-service-due', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(report, null, 2),
},
],
};
},
},
];
}

View File

@ -0,0 +1,140 @@
/**
* Reporting and Analytics Tools
*/
import type { Report, RevenueReport, TechnicianProductivityReport } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const reportingTools = [
{
name: 'fieldedge_get_revenue_report',
description: 'Get revenue report for a period',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
groupBy: { type: 'string', enum: ['day', 'week', 'month'], default: 'day' },
},
required: ['startDate', 'endDate'],
},
},
{
name: 'fieldedge_get_technician_productivity_report',
description: 'Get technician productivity metrics',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string' },
endDate: { type: 'string' },
technicianIds: { type: 'array', items: { type: 'string' } },
},
required: ['startDate', 'endDate'],
},
},
{
name: 'fieldedge_get_job_completion_report',
description: 'Get job completion statistics',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string' },
endDate: { type: 'string' },
jobType: { type: 'string' },
status: { type: 'string' },
},
required: ['startDate', 'endDate'],
},
},
{
name: 'fieldedge_get_aging_receivables_report',
description: 'Get accounts receivable aging report',
inputSchema: {
type: 'object',
properties: {
asOfDate: { type: 'string', description: 'As of date (YYYY-MM-DD)' },
customerId: { type: 'string' },
},
},
},
{
name: 'fieldedge_get_sales_by_category_report',
description: 'Get sales breakdown by category',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string' },
endDate: { type: 'string' },
},
required: ['startDate', 'endDate'],
},
},
{
name: 'fieldedge_get_equipment_maintenance_report',
description: 'Get equipment maintenance history and upcoming',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string' },
equipmentType: { type: 'string' },
overdueOnly: { type: 'boolean' },
},
},
},
{
name: 'fieldedge_get_customer_satisfaction_report',
description: 'Get customer satisfaction metrics',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string' },
endDate: { type: 'string' },
},
required: ['startDate', 'endDate'],
},
},
{
name: 'fieldedge_get_inventory_valuation_report',
description: 'Get current inventory valuation',
inputSchema: {
type: 'object',
properties: {
warehouse: { type: 'string' },
category: { type: 'string' },
},
},
},
];
export async function handleReportingTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_get_revenue_report':
return await client.get<RevenueReport>('/reports/revenue', args);
case 'fieldedge_get_technician_productivity_report':
return await client.get<TechnicianProductivityReport>('/reports/technician-productivity', args);
case 'fieldedge_get_job_completion_report':
return await client.get('/reports/job-completion', args);
case 'fieldedge_get_aging_receivables_report':
return await client.get('/reports/aging-receivables', args);
case 'fieldedge_get_sales_by_category_report':
return await client.get('/reports/sales-by-category', args);
case 'fieldedge_get_equipment_maintenance_report':
return await client.get('/reports/equipment-maintenance', args);
case 'fieldedge_get_customer_satisfaction_report':
return await client.get('/reports/customer-satisfaction', args);
case 'fieldedge_get_inventory_valuation_report':
return await client.get('/reports/inventory-valuation', args);
default:
throw new Error(`Unknown reporting tool: ${name}`);
}
}

View File

@ -0,0 +1,175 @@
/**
* Scheduling and Dispatch Tools
*/
import type { Appointment, DispatchBoard } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const schedulingTools = [
{
name: 'fieldedge_list_appointments',
description: 'List appointments with filtering',
inputSchema: {
type: 'object',
properties: {
startDate: { type: 'string' },
endDate: { type: 'string' },
technicianId: { type: 'string' },
customerId: { type: 'string' },
status: { type: 'string', enum: ['scheduled', 'confirmed', 'dispatched', 'en-route', 'arrived', 'completed', 'cancelled', 'no-show'] },
page: { type: 'number' },
pageSize: { type: 'number' },
},
},
},
{
name: 'fieldedge_get_appointment',
description: 'Get specific appointment',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_appointment',
description: 'Create a new appointment',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string' },
customerId: { type: 'string' },
technicianId: { type: 'string' },
startTime: { type: 'string' },
endTime: { type: 'string' },
appointmentType: { type: 'string' },
arrivalWindow: {
type: 'object',
properties: {
start: { type: 'string' },
end: { type: 'string' },
},
},
notes: { type: 'string' },
},
required: ['jobId', 'customerId', 'technicianId', 'startTime', 'endTime', 'appointmentType'],
},
},
{
name: 'fieldedge_update_appointment',
description: 'Update an appointment',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
startTime: { type: 'string' },
endTime: { type: 'string' },
technicianId: { type: 'string' },
status: { type: 'string', enum: ['scheduled', 'confirmed', 'dispatched', 'en-route', 'arrived', 'completed', 'cancelled', 'no-show'] },
notes: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_cancel_appointment',
description: 'Cancel an appointment',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
reason: { type: 'string' },
notifyCustomer: { type: 'boolean', default: true },
},
required: ['id'],
},
},
{
name: 'fieldedge_get_dispatch_board',
description: 'Get dispatch board for a specific date',
inputSchema: {
type: 'object',
properties: {
date: { type: 'string', description: 'Date (YYYY-MM-DD)' },
technicianIds: { type: 'array', items: { type: 'string' }, description: 'Filter by specific technicians' },
},
required: ['date'],
},
},
{
name: 'fieldedge_dispatch_job',
description: 'Dispatch a job to technician',
inputSchema: {
type: 'object',
properties: {
jobId: { type: 'string' },
technicianId: { type: 'string' },
scheduledTime: { type: 'string' },
notifyTechnician: { type: 'boolean', default: true },
},
required: ['jobId', 'technicianId', 'scheduledTime'],
},
},
{
name: 'fieldedge_optimize_routes',
description: 'Optimize technician routes for a day',
inputSchema: {
type: 'object',
properties: {
date: { type: 'string' },
technicianIds: { type: 'array', items: { type: 'string' } },
},
required: ['date'],
},
},
];
export async function handleSchedulingTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_appointments':
return await client.getPaginated<Appointment>('/appointments', args);
case 'fieldedge_get_appointment':
return await client.get<Appointment>(`/appointments/${args.id}`);
case 'fieldedge_create_appointment':
return await client.post<Appointment>('/appointments', args);
case 'fieldedge_update_appointment':
const { id, ...updates } = args;
return await client.patch<Appointment>(`/appointments/${id}`, updates);
case 'fieldedge_cancel_appointment':
return await client.post(`/appointments/${args.id}/cancel`, {
reason: args.reason,
notifyCustomer: args.notifyCustomer,
});
case 'fieldedge_get_dispatch_board':
return await client.get<DispatchBoard>('/dispatch/board', {
date: args.date,
technicianIds: args.technicianIds,
});
case 'fieldedge_dispatch_job':
return await client.post('/dispatch', {
jobId: args.jobId,
technicianId: args.technicianId,
scheduledTime: args.scheduledTime,
notifyTechnician: args.notifyTechnician,
});
case 'fieldedge_optimize_routes':
return await client.post('/dispatch/optimize-routes', {
date: args.date,
technicianIds: args.technicianIds,
});
default:
throw new Error(`Unknown scheduling tool: ${name}`);
}
}

View File

@ -0,0 +1,139 @@
/**
* Service Agreement Management Tools
*/
import type { ServiceAgreement } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const serviceAgreementTools = [
{
name: 'fieldedge_list_service_agreements',
description: 'List service agreements',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string' },
status: { type: 'string', enum: ['active', 'expired', 'cancelled'] },
type: { type: 'string', enum: ['maintenance', 'warranty', 'service-plan'] },
},
},
},
{
name: 'fieldedge_get_service_agreement',
description: 'Get specific service agreement',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_service_agreement',
description: 'Create new service agreement',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string' },
name: { type: 'string' },
type: { type: 'string', enum: ['maintenance', 'warranty', 'service-plan'] },
startDate: { type: 'string' },
endDate: { type: 'string' },
billingCycle: { type: 'string', enum: ['monthly', 'quarterly', 'annual'] },
amount: { type: 'number' },
autoRenew: { type: 'boolean', default: false },
services: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
frequency: { type: 'string', enum: ['weekly', 'monthly', 'quarterly', 'semi-annual', 'annual'] },
},
},
},
equipmentIds: { type: 'array', items: { type: 'string' } },
notes: { type: 'string' },
},
required: ['customerId', 'name', 'type', 'startDate', 'endDate', 'billingCycle', 'amount'],
},
},
{
name: 'fieldedge_update_service_agreement',
description: 'Update service agreement',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
status: { type: 'string', enum: ['active', 'expired', 'cancelled'] },
endDate: { type: 'string' },
amount: { type: 'number' },
autoRenew: { type: 'boolean' },
notes: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_cancel_service_agreement',
description: 'Cancel a service agreement',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
reason: { type: 'string' },
effectiveDate: { type: 'string' },
},
required: ['id', 'reason'],
},
},
{
name: 'fieldedge_renew_service_agreement',
description: 'Renew an expiring service agreement',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
newEndDate: { type: 'string' },
newAmount: { type: 'number' },
},
required: ['id', 'newEndDate'],
},
},
];
export async function handleServiceAgreementTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_service_agreements':
return await client.getPaginated<ServiceAgreement>('/service-agreements', args);
case 'fieldedge_get_service_agreement':
return await client.get<ServiceAgreement>(`/service-agreements/${args.id}`);
case 'fieldedge_create_service_agreement':
return await client.post<ServiceAgreement>('/service-agreements', args);
case 'fieldedge_update_service_agreement':
const { id, ...updates } = args;
return await client.patch<ServiceAgreement>(`/service-agreements/${id}`, updates);
case 'fieldedge_cancel_service_agreement':
return await client.post(`/service-agreements/${args.id}/cancel`, {
reason: args.reason,
effectiveDate: args.effectiveDate,
});
case 'fieldedge_renew_service_agreement':
return await client.post(`/service-agreements/${args.id}/renew`, {
newEndDate: args.newEndDate,
newAmount: args.newAmount,
});
default:
throw new Error(`Unknown service agreement tool: ${name}`);
}
}

View File

@ -0,0 +1,125 @@
/**
* Task Management Tools
*/
import type { Task } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const taskTools = [
{
name: 'fieldedge_list_tasks',
description: 'List tasks',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', enum: ['pending', 'in-progress', 'completed', 'cancelled'] },
priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] },
assignedTo: { type: 'string' },
customerId: { type: 'string' },
jobId: { type: 'string' },
dueDate: { type: 'string' },
},
},
},
{
name: 'fieldedge_get_task',
description: 'Get specific task',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_task',
description: 'Create new task',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string' },
description: { type: 'string' },
type: { type: 'string', enum: ['call', 'email', 'follow-up', 'inspection', 'other'] },
priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'], default: 'normal' },
dueDate: { type: 'string' },
assignedTo: { type: 'string' },
customerId: { type: 'string' },
jobId: { type: 'string' },
notes: { type: 'string' },
},
required: ['title', 'description', 'type'],
},
},
{
name: 'fieldedge_update_task',
description: 'Update task',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
status: { type: 'string', enum: ['pending', 'in-progress', 'completed', 'cancelled'] },
priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] },
dueDate: { type: 'string' },
assignedTo: { type: 'string' },
notes: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_complete_task',
description: 'Mark task as completed',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
notes: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_delete_task',
description: 'Delete a task',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
];
export async function handleTaskTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_tasks':
return await client.getPaginated<Task>('/tasks', args);
case 'fieldedge_get_task':
return await client.get<Task>(`/tasks/${args.id}`);
case 'fieldedge_create_task':
return await client.post<Task>('/tasks', args);
case 'fieldedge_update_task':
const { id, ...updates } = args;
return await client.patch<Task>(`/tasks/${id}`, updates);
case 'fieldedge_complete_task':
return await client.patch<Task>(`/tasks/${args.id}`, {
status: 'completed',
completedDate: new Date().toISOString(),
notes: args.notes,
});
case 'fieldedge_delete_task':
return await client.delete(`/tasks/${args.id}`);
default:
throw new Error(`Unknown task tool: ${name}`);
}
}

View File

@ -1,234 +0,0 @@
/**
* FieldEdge Technicians Tools
*/
import { FieldEdgeClient } from '../client.js';
import { Technician, TechnicianPerformance, TimeEntry, PaginationParams } from '../types.js';
export function createTechniciansTools(client: FieldEdgeClient) {
return [
{
name: 'fieldedge_technicians_list',
description: 'List all technicians with optional filtering',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
description: 'Filter by technician status',
enum: ['active', 'inactive', 'on_leave'],
},
role: { type: 'string', description: 'Filter by role' },
page: { type: 'number' },
pageSize: { type: 'number' },
},
},
handler: async (params: PaginationParams & {
status?: string;
role?: string;
}) => {
const result = await client.getPaginated<Technician>('/technicians', params as any);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'fieldedge_technicians_get',
description: 'Get detailed information about a specific technician',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'string', description: 'Technician ID' },
},
required: ['technicianId'],
},
handler: async (params: { technicianId: string }) => {
const technician = await client.get<Technician>(
`/technicians/${params.technicianId}`
);
return {
content: [
{
type: 'text',
text: JSON.stringify(technician, null, 2),
},
],
};
},
},
{
name: 'fieldedge_technicians_create',
description: 'Create a new technician',
inputSchema: {
type: 'object',
properties: {
firstName: { type: 'string', description: 'First name' },
lastName: { type: 'string', description: 'Last name' },
email: { type: 'string', description: 'Email address' },
phone: { type: 'string', description: 'Phone number' },
role: { type: 'string', description: 'Job role/title' },
hourlyRate: { type: 'number', description: 'Hourly rate' },
hireDate: { type: 'string', description: 'Hire date (ISO 8601)' },
certifications: {
type: 'array',
items: { type: 'string' },
description: 'List of certifications',
},
skills: {
type: 'array',
items: { type: 'string' },
description: 'List of skills',
},
},
required: ['firstName', 'lastName'],
},
handler: async (params: {
firstName: string;
lastName: string;
email?: string;
phone?: string;
role?: string;
hourlyRate?: number;
hireDate?: string;
certifications?: string[];
skills?: string[];
}) => {
const technician = await client.post<Technician>('/technicians', params);
return {
content: [
{
type: 'text',
text: JSON.stringify(technician, null, 2),
},
],
};
},
},
{
name: 'fieldedge_technicians_update',
description: 'Update an existing technician',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'string', description: 'Technician ID' },
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
status: {
type: 'string',
enum: ['active', 'inactive', 'on_leave'],
},
role: { type: 'string' },
hourlyRate: { type: 'number' },
certifications: {
type: 'array',
items: { type: 'string' },
},
skills: {
type: 'array',
items: { type: 'string' },
},
},
required: ['technicianId'],
},
handler: async (params: {
technicianId: string;
firstName?: string;
lastName?: string;
email?: string;
phone?: string;
status?: string;
role?: string;
hourlyRate?: number;
certifications?: string[];
skills?: string[];
}) => {
const { technicianId, ...updateData } = params;
const technician = await client.patch<Technician>(
`/technicians/${technicianId}`,
updateData
);
return {
content: [
{
type: 'text',
text: JSON.stringify(technician, null, 2),
},
],
};
},
},
{
name: 'fieldedge_technicians_performance_get',
description: 'Get performance metrics for a technician',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'string', description: 'Technician ID' },
startDate: { type: 'string', description: 'Period start date (ISO 8601)' },
endDate: { type: 'string', description: 'Period end date (ISO 8601)' },
},
required: ['technicianId'],
},
handler: async (params: {
technicianId: string;
startDate?: string;
endDate?: string;
}) => {
const performance = await client.get<TechnicianPerformance>(
`/technicians/${params.technicianId}/performance`,
params
);
return {
content: [
{
type: 'text',
text: JSON.stringify(performance, null, 2),
},
],
};
},
},
{
name: 'fieldedge_technicians_time_entries_list',
description: 'List time entries for a technician',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'string', description: 'Technician ID' },
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
endDate: { type: 'string', description: 'End date (ISO 8601)' },
page: { type: 'number' },
pageSize: { type: 'number' },
},
required: ['technicianId'],
},
handler: async (params: PaginationParams & {
technicianId: string;
startDate?: string;
endDate?: string;
}) => {
const { technicianId, ...queryParams } = params;
const result = await client.getPaginated<TimeEntry>(
`/technicians/${technicianId}/time-entries`,
queryParams
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];
}

View File

@ -0,0 +1,184 @@
/**
* Technician Management Tools
*/
import type { Technician, TimeEntry } from '../types/index.js';
import { getFieldEdgeClient } from '../clients/fieldedge.js';
export const technicianTools = [
{
name: 'fieldedge_list_technicians',
description: 'List all technicians',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number' },
pageSize: { type: 'number' },
status: { type: 'string', enum: ['active', 'inactive', 'on-leave'] },
skills: { type: 'array', items: { type: 'string' } },
},
},
},
{
name: 'fieldedge_get_technician',
description: 'Get specific technician by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_create_technician',
description: 'Create a new technician',
inputSchema: {
type: 'object',
properties: {
employeeNumber: { type: 'string' },
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
role: { type: 'string' },
skills: { type: 'array', items: { type: 'string' } },
hourlyRate: { type: 'number' },
overtimeRate: { type: 'number' },
serviceRadius: { type: 'number' },
},
required: ['employeeNumber', 'firstName', 'lastName', 'email', 'phone', 'role'],
},
},
{
name: 'fieldedge_update_technician',
description: 'Update technician details',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
status: { type: 'string', enum: ['active', 'inactive', 'on-leave'] },
email: { type: 'string' },
phone: { type: 'string' },
role: { type: 'string' },
skills: { type: 'array', items: { type: 'string' } },
hourlyRate: { type: 'number' },
overtimeRate: { type: 'number' },
},
required: ['id'],
},
},
{
name: 'fieldedge_delete_technician',
description: 'Delete a technician',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
},
{
name: 'fieldedge_get_technician_schedule',
description: 'Get technician schedule for a date range',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
},
required: ['id', 'startDate', 'endDate'],
},
},
{
name: 'fieldedge_get_technician_availability',
description: 'Get technician availability for scheduling',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' },
date: { type: 'string' },
},
required: ['id', 'date'],
},
},
{
name: 'fieldedge_clock_in_technician',
description: 'Clock in technician (start time tracking)',
inputSchema: {
type: 'object',
properties: {
technicianId: { type: 'string' },
jobId: { type: 'string' },
notes: { type: 'string' },
},
required: ['technicianId'],
},
},
{
name: 'fieldedge_clock_out_technician',
description: 'Clock out technician (end time tracking)',
inputSchema: {
type: 'object',
properties: {
timeEntryId: { type: 'string' },
notes: { type: 'string' },
},
required: ['timeEntryId'],
},
},
];
export async function handleTechnicianTool(name: string, args: any): Promise<any> {
const client = getFieldEdgeClient();
switch (name) {
case 'fieldedge_list_technicians':
return await client.getPaginated<Technician>('/technicians', args);
case 'fieldedge_get_technician':
return await client.get<Technician>(`/technicians/${args.id}`);
case 'fieldedge_create_technician':
return await client.post<Technician>('/technicians', args);
case 'fieldedge_update_technician':
const { id, ...updates } = args;
return await client.patch<Technician>(`/technicians/${id}`, updates);
case 'fieldedge_delete_technician':
return await client.delete(`/technicians/${args.id}`);
case 'fieldedge_get_technician_schedule':
return await client.get(`/technicians/${args.id}/schedule`, {
startDate: args.startDate,
endDate: args.endDate,
});
case 'fieldedge_get_technician_availability':
return await client.get(`/technicians/${args.id}/availability`, {
date: args.date,
});
case 'fieldedge_clock_in_technician':
return await client.post<TimeEntry>('/time-entries', {
technicianId: args.technicianId,
jobId: args.jobId,
startTime: new Date().toISOString(),
type: 'regular',
billable: true,
notes: args.notes,
});
case 'fieldedge_clock_out_technician':
return await client.patch<TimeEntry>(`/time-entries/${args.timeEntryId}`, {
endTime: new Date().toISOString(),
notes: args.notes,
});
default:
throw new Error(`Unknown technician tool: ${name}`);
}
}

View File

@ -1,439 +0,0 @@
/**
* FieldEdge MCP Server - Type Definitions
*/
// API Configuration
export interface FieldEdgeConfig {
apiKey: string;
baseUrl?: string;
}
// Common Types
export interface PaginationParams {
page?: number;
pageSize?: number;
offset?: number;
limit?: number;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}
export interface ApiError {
message: string;
code?: string;
statusCode?: number;
details?: unknown;
}
// Job Types
export interface Job {
id: string;
jobNumber: string;
customerId: string;
customerName?: string;
locationId?: string;
jobType: string;
status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'on_hold';
priority: 'low' | 'normal' | 'high' | 'emergency';
assignedTechId?: string;
assignedTechName?: string;
scheduledStart?: string;
scheduledEnd?: string;
actualStart?: string;
actualEnd?: string;
description?: string;
notes?: string;
totalAmount?: number;
address?: Address;
createdAt: string;
updatedAt: string;
}
export interface JobLineItem {
id: string;
jobId: string;
type: 'labor' | 'material' | 'equipment' | 'other';
description: string;
quantity: number;
unitPrice: number;
totalPrice: number;
taxable: boolean;
partNumber?: string;
technicianId?: string;
}
export interface JobEquipment {
id: string;
jobId: string;
equipmentId: string;
equipmentType: string;
serialNumber?: string;
model?: string;
manufacturer?: string;
serviceType: string;
}
// Customer Types
export interface Customer {
id: string;
customerNumber: string;
type: 'residential' | 'commercial';
firstName?: string;
lastName?: string;
companyName?: string;
email?: string;
phone?: string;
mobilePhone?: string;
status: 'active' | 'inactive';
balance: number;
creditLimit?: number;
taxExempt: boolean;
primaryAddress?: Address;
billingAddress?: Address;
tags?: string[];
createdAt: string;
updatedAt: string;
}
export interface CustomerLocation {
id: string;
customerId: string;
name: string;
address: Address;
isPrimary: boolean;
contactName?: string;
contactPhone?: string;
accessNotes?: string;
gateCode?: string;
}
export interface Address {
street1: string;
street2?: string;
city: string;
state: string;
zip: string;
country?: string;
latitude?: number;
longitude?: number;
}
// Invoice Types
export interface Invoice {
id: string;
invoiceNumber: string;
customerId: string;
customerName?: string;
jobId?: string;
status: 'draft' | 'sent' | 'paid' | 'partial' | 'overdue' | 'void';
invoiceDate: string;
dueDate?: string;
subtotal: number;
taxAmount: number;
totalAmount: number;
amountPaid: number;
amountDue: number;
lineItems: InvoiceLineItem[];
notes?: string;
terms?: string;
createdAt: string;
updatedAt: string;
}
export interface InvoiceLineItem {
id: string;
type: 'labor' | 'material' | 'equipment' | 'other';
description: string;
quantity: number;
unitPrice: number;
totalPrice: number;
taxable: boolean;
}
export interface Payment {
id: string;
invoiceId: string;
amount: number;
paymentMethod: 'cash' | 'check' | 'credit_card' | 'ach' | 'other';
paymentDate: string;
referenceNumber?: string;
notes?: string;
createdAt: string;
}
// Estimate Types
export interface Estimate {
id: string;
estimateNumber: string;
customerId: string;
customerName?: string;
locationId?: string;
status: 'draft' | 'sent' | 'approved' | 'declined' | 'expired';
estimateDate: string;
expirationDate?: string;
subtotal: number;
taxAmount: number;
totalAmount: number;
lineItems: EstimateLineItem[];
notes?: string;
terms?: string;
createdBy?: string;
createdAt: string;
updatedAt: string;
}
export interface EstimateLineItem {
id: string;
type: 'labor' | 'material' | 'equipment' | 'other';
description: string;
quantity: number;
unitPrice: number;
totalPrice: number;
taxable: boolean;
}
// Technician Types
export interface Technician {
id: string;
employeeNumber: string;
firstName: string;
lastName: string;
email?: string;
phone?: string;
status: 'active' | 'inactive' | 'on_leave';
role: string;
certifications?: string[];
skills?: string[];
hourlyRate?: number;
hireDate?: string;
terminationDate?: string;
createdAt: string;
updatedAt: string;
}
export interface TechnicianPerformance {
technicianId: string;
technicianName: string;
period: string;
jobsCompleted: number;
averageJobTime: number;
revenue: number;
customerSatisfaction?: number;
callbackRate?: number;
efficiency?: number;
}
export interface TimeEntry {
id: string;
technicianId: string;
jobId?: string;
date: string;
clockIn: string;
clockOut?: string;
hours: number;
type: 'regular' | 'overtime' | 'double_time' | 'travel';
notes?: string;
}
// Dispatch Types
export interface DispatchBoard {
date: string;
zones: DispatchZone[];
unassignedJobs: Job[];
}
export interface DispatchZone {
id: string;
name: string;
technicians: DispatchTechnician[];
}
export interface DispatchTechnician {
id: string;
name: string;
status: 'available' | 'on_job' | 'traveling' | 'break' | 'offline';
currentLocation?: { lat: number; lng: number };
assignedJobs: Job[];
capacity: number;
utilizationPercent: number;
}
export interface TechnicianAvailability {
technicianId: string;
date: string;
availableSlots: TimeSlot[];
bookedSlots: TimeSlot[];
}
export interface TimeSlot {
start: string;
end: string;
duration: number;
}
// Equipment Types
export interface Equipment {
id: string;
customerId: string;
locationId?: string;
type: string;
manufacturer?: string;
model?: string;
serialNumber?: string;
installDate?: string;
warrantyExpiration?: string;
status: 'active' | 'inactive' | 'retired';
lastServiceDate?: string;
nextServiceDate?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export interface ServiceHistory {
id: string;
equipmentId: string;
jobId: string;
technicianId: string;
technicianName?: string;
serviceDate: string;
serviceType: string;
description: string;
partsUsed?: string[];
laborHours?: number;
cost?: number;
notes?: string;
}
// Inventory Types
export interface InventoryPart {
id: string;
partNumber: string;
description: string;
category?: string;
manufacturer?: string;
quantityOnHand: number;
quantityAvailable: number;
quantityOnOrder: number;
reorderPoint?: number;
reorderQuantity?: number;
unitCost: number;
unitPrice: number;
location?: string;
updatedAt: string;
}
export interface PurchaseOrder {
id: string;
poNumber: string;
vendorId: string;
vendorName?: string;
status: 'draft' | 'submitted' | 'approved' | 'received' | 'cancelled';
orderDate: string;
expectedDate?: string;
receivedDate?: string;
subtotal: number;
taxAmount: number;
totalAmount: number;
lineItems: PurchaseOrderLineItem[];
notes?: string;
createdAt: string;
updatedAt: string;
}
export interface PurchaseOrderLineItem {
id: string;
partId: string;
partNumber: string;
description: string;
quantityOrdered: number;
quantityReceived: number;
unitCost: number;
totalCost: number;
}
// Service Agreement Types
export interface ServiceAgreement {
id: string;
agreementNumber: string;
customerId: string;
customerName?: string;
locationId?: string;
type: string;
status: 'active' | 'cancelled' | 'expired' | 'suspended';
startDate: string;
endDate?: string;
renewalDate?: string;
billingFrequency: 'monthly' | 'quarterly' | 'annually';
amount: number;
equipmentCovered?: string[];
servicesCovered?: string[];
visitsPerYear?: number;
visitsRemaining?: number;
autoRenew: boolean;
notes?: string;
createdAt: string;
updatedAt: string;
}
// Reporting Types
export interface RevenueReport {
period: string;
startDate: string;
endDate: string;
totalRevenue: number;
laborRevenue: number;
partsRevenue: number;
equipmentRevenue: number;
agreementRevenue: number;
jobCount: number;
averageTicket: number;
byJobType?: Record<string, number>;
byTechnician?: Record<string, number>;
}
export interface JobProfitabilityReport {
jobId: string;
jobNumber: string;
revenue: number;
laborCost: number;
partsCost: number;
overheadCost: number;
totalCost: number;
profit: number;
profitMargin: number;
}
export interface AgingReport {
asOfDate: string;
totalOutstanding: number;
current: AgingBucket;
days30: AgingBucket;
days60: AgingBucket;
days90: AgingBucket;
days90Plus: AgingBucket;
byCustomer: CustomerAging[];
}
export interface AgingBucket {
amount: number;
invoiceCount: number;
percentage: number;
}
export interface CustomerAging {
customerId: string;
customerName: string;
totalDue: number;
current: number;
days30: number;
days60: number;
days90: number;
days90Plus: number;
}

View File

@ -0,0 +1,602 @@
/**
* FieldEdge MCP Server Type Definitions
* Comprehensive types for field service management
*/
export interface FieldEdgeConfig {
apiKey: string;
apiUrl?: string;
companyId?: string;
timeout?: number;
}
// Customer Types
export interface Customer {
id: string;
firstName: string;
lastName: string;
companyName?: string;
email?: string;
phone?: string;
mobilePhone?: string;
address?: Address;
billingAddress?: Address;
status: 'active' | 'inactive' | 'prospect';
customerType: 'residential' | 'commercial';
balance: number;
creditLimit?: number;
taxExempt: boolean;
notes?: string;
tags?: string[];
customFields?: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export interface Address {
street1: string;
street2?: string;
city: string;
state: string;
zip: string;
country?: string;
latitude?: number;
longitude?: number;
}
// Job/Work Order Types
export interface Job {
id: string;
jobNumber: string;
customerId: string;
locationId?: string;
jobType: string;
status: JobStatus;
priority: 'low' | 'normal' | 'high' | 'emergency';
description: string;
scheduledStart?: string;
scheduledEnd?: string;
actualStart?: string;
actualEnd?: string;
assignedTechnicians: string[];
equipmentIds?: string[];
subtotal: number;
tax: number;
total: number;
invoiceId?: string;
tags?: string[];
customFields?: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export type JobStatus =
| 'scheduled'
| 'dispatched'
| 'in-progress'
| 'on-hold'
| 'completed'
| 'cancelled'
| 'invoiced';
// Invoice Types
export interface Invoice {
id: string;
invoiceNumber: string;
customerId: string;
jobId?: string;
status: InvoiceStatus;
issueDate: string;
dueDate: string;
paidDate?: string;
lineItems: LineItem[];
subtotal: number;
tax: number;
discount: number;
total: number;
amountPaid: number;
balance: number;
paymentTerms?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export type InvoiceStatus = 'draft' | 'sent' | 'viewed' | 'partial' | 'paid' | 'overdue' | 'void';
export interface LineItem {
id: string;
type: 'service' | 'part' | 'equipment' | 'labor';
description: string;
quantity: number;
unitPrice: number;
discount: number;
tax: number;
total: number;
itemId?: string;
}
// Estimate Types
export interface Estimate {
id: string;
estimateNumber: string;
customerId: string;
status: EstimateStatus;
issueDate: string;
expiryDate: string;
lineItems: LineItem[];
subtotal: number;
tax: number;
discount: number;
total: number;
approvedDate?: string;
jobId?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export type EstimateStatus = 'draft' | 'sent' | 'viewed' | 'approved' | 'declined' | 'expired';
// Scheduling/Dispatch Types
export interface Appointment {
id: string;
jobId: string;
customerId: string;
technicianId: string;
startTime: string;
endTime: string;
duration: number;
status: AppointmentStatus;
appointmentType: string;
arrivalWindow?: {
start: string;
end: string;
};
actualArrival?: string;
actualDeparture?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export type AppointmentStatus = 'scheduled' | 'confirmed' | 'dispatched' | 'en-route' | 'arrived' | 'completed' | 'cancelled' | 'no-show';
export interface DispatchBoard {
date: string;
technicians: TechnicianSchedule[];
unassignedJobs: Job[];
}
export interface TechnicianSchedule {
technicianId: string;
technicianName: string;
appointments: Appointment[];
availability: TimeSlot[];
capacity: number;
}
export interface TimeSlot {
start: string;
end: string;
available: boolean;
}
// Inventory Types
export interface InventoryItem {
id: string;
sku: string;
name: string;
description?: string;
category: string;
manufacturer?: string;
modelNumber?: string;
unitOfMeasure: string;
costPrice: number;
sellPrice: number;
msrp?: number;
quantityOnHand: number;
quantityAllocated: number;
quantityAvailable: number;
reorderPoint: number;
reorderQuantity: number;
warehouse?: string;
binLocation?: string;
serialized: boolean;
taxable: boolean;
tags?: string[];
createdAt: string;
updatedAt: string;
}
export interface InventoryTransaction {
id: string;
itemId: string;
type: 'receipt' | 'issue' | 'adjustment' | 'transfer' | 'return';
quantity: number;
unitCost?: number;
totalCost?: number;
reference?: string;
jobId?: string;
technicianId?: string;
notes?: string;
createdAt: string;
}
// Technician Types
export interface Technician {
id: string;
employeeNumber: string;
firstName: string;
lastName: string;
email: string;
phone: string;
status: 'active' | 'inactive' | 'on-leave';
role: string;
skills: string[];
certifications: Certification[];
hourlyRate?: number;
overtimeRate?: number;
serviceRadius?: number;
homeAddress?: Address;
vehicleId?: string;
defaultWorkHours?: WorkHours;
createdAt: string;
updatedAt: string;
}
export interface Certification {
name: string;
number: string;
issuer: string;
issueDate: string;
expiryDate?: string;
}
export interface WorkHours {
monday?: DaySchedule;
tuesday?: DaySchedule;
wednesday?: DaySchedule;
thursday?: DaySchedule;
friday?: DaySchedule;
saturday?: DaySchedule;
sunday?: DaySchedule;
}
export interface DaySchedule {
start: string;
end: string;
breaks?: TimeSlot[];
}
// Payment Types
export interface Payment {
id: string;
invoiceId: string;
customerId: string;
amount: number;
paymentMethod: PaymentMethod;
paymentDate: string;
reference?: string;
cardLast4?: string;
checkNumber?: string;
status: 'pending' | 'processed' | 'failed' | 'refunded';
notes?: string;
createdAt: string;
updatedAt: string;
}
export type PaymentMethod = 'cash' | 'check' | 'credit-card' | 'debit-card' | 'ach' | 'wire' | 'other';
// Equipment Types
export interface Equipment {
id: string;
customerId: string;
locationId?: string;
type: string;
manufacturer: string;
model: string;
serialNumber?: string;
installDate?: string;
warrantyExpiry?: string;
lastServiceDate?: string;
nextServiceDue?: string;
status: 'active' | 'inactive' | 'decommissioned';
notes?: string;
customFields?: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export interface ServiceHistory {
id: string;
equipmentId: string;
jobId: string;
serviceDate: string;
technicianId: string;
serviceType: string;
description: string;
partsReplaced?: LineItem[];
cost: number;
notes?: string;
}
// Location Types
export interface Location {
id: string;
customerId: string;
name: string;
address: Address;
type: 'primary' | 'secondary' | 'billing' | 'service';
contactName?: string;
contactPhone?: string;
accessNotes?: string;
gateCode?: string;
equipmentIds?: string[];
status: 'active' | 'inactive';
createdAt: string;
updatedAt: string;
}
// Price Book Types
export interface PriceBook {
id: string;
name: string;
description?: string;
effectiveDate: string;
expiryDate?: string;
isDefault: boolean;
status: 'active' | 'inactive';
items: PriceBookItem[];
createdAt: string;
updatedAt: string;
}
export interface PriceBookItem {
id: string;
itemId: string;
itemType: 'service' | 'part' | 'equipment' | 'labor';
name: string;
sku?: string;
description?: string;
unitPrice: number;
costPrice?: number;
margin?: number;
taxable: boolean;
category?: string;
}
// Service Agreement Types
export interface ServiceAgreement {
id: string;
customerId: string;
agreementNumber: string;
name: string;
type: 'maintenance' | 'warranty' | 'service-plan';
status: 'active' | 'expired' | 'cancelled';
startDate: string;
endDate: string;
renewalDate?: string;
autoRenew: boolean;
billingCycle: 'monthly' | 'quarterly' | 'annual';
amount: number;
services: ServiceItem[];
equipmentIds?: string[];
notes?: string;
createdAt: string;
updatedAt: string;
}
export interface ServiceItem {
id: string;
name: string;
description: string;
frequency: 'weekly' | 'monthly' | 'quarterly' | 'semi-annual' | 'annual';
nextDue?: string;
lastCompleted?: string;
}
// Task Types
export interface Task {
id: string;
jobId?: string;
customerId?: string;
technicianId?: string;
title: string;
description: string;
type: 'call' | 'email' | 'follow-up' | 'inspection' | 'other';
priority: 'low' | 'normal' | 'high' | 'urgent';
status: 'pending' | 'in-progress' | 'completed' | 'cancelled';
dueDate?: string;
completedDate?: string;
assignedTo?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
// Call Tracking Types
export interface Call {
id: string;
customerId?: string;
phone: string;
direction: 'inbound' | 'outbound';
callType: 'inquiry' | 'booking' | 'follow-up' | 'support' | 'sales';
status: 'answered' | 'missed' | 'voicemail' | 'busy';
duration?: number;
recordingUrl?: string;
notes?: string;
outcome?: string;
jobId?: string;
technicianId?: string;
timestamp: string;
}
// Reporting Types
export interface Report {
id: string;
name: string;
type: ReportType;
parameters?: Record<string, any>;
generatedAt: string;
data: any;
}
export type ReportType =
| 'revenue'
| 'technician-productivity'
| 'job-completion'
| 'customer-satisfaction'
| 'inventory-valuation'
| 'aging-receivables'
| 'sales-by-category'
| 'equipment-maintenance'
| 'custom';
export interface RevenueReport {
period: string;
totalRevenue: number;
invoicedAmount: number;
collectedAmount: number;
outstandingAmount: number;
byCategory: Record<string, number>;
byTechnician: Record<string, number>;
trend: RevenueDataPoint[];
}
export interface RevenueDataPoint {
date: string;
revenue: number;
jobs: number;
}
export interface TechnicianProductivityReport {
period: string;
technicians: TechnicianMetrics[];
}
export interface TechnicianMetrics {
technicianId: string;
technicianName: string;
jobsCompleted: number;
hoursWorked: number;
revenue: number;
averageJobTime: number;
utilizationRate: number;
}
// API Response Types
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface QueryParams {
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
filter?: Record<string, any>;
search?: string;
[key: string]: any; // Allow additional query parameters
}
// Vehicle Types
export interface Vehicle {
id: string;
name: string;
type: 'truck' | 'van' | 'car' | 'trailer';
make: string;
model: string;
year: number;
vin?: string;
licensePlate: string;
status: 'active' | 'maintenance' | 'inactive';
assignedTechnicianId?: string;
mileage?: number;
lastServiceDate?: string;
nextServiceDue?: string;
inventoryItems?: string[];
gpsEnabled: boolean;
createdAt: string;
updatedAt: string;
}
// Time Tracking Types
export interface TimeEntry {
id: string;
technicianId: string;
jobId?: string;
type: 'regular' | 'overtime' | 'travel' | 'break';
startTime: string;
endTime?: string;
duration?: number;
billable: boolean;
approved: boolean;
notes?: string;
createdAt: string;
updatedAt: string;
}
// Forms/Checklist Types
export interface Form {
id: string;
name: string;
description?: string;
type: 'inspection' | 'safety' | 'maintenance' | 'quote' | 'custom';
status: 'active' | 'inactive';
fields: FormField[];
createdAt: string;
updatedAt: string;
}
export interface FormField {
id: string;
label: string;
type: 'text' | 'number' | 'checkbox' | 'select' | 'radio' | 'date' | 'signature' | 'photo';
required: boolean;
options?: string[];
defaultValue?: any;
validationRules?: Record<string, any>;
}
export interface FormSubmission {
id: string;
formId: string;
jobId?: string;
technicianId: string;
customerId?: string;
responses: Record<string, any>;
attachments?: string[];
submittedAt: string;
}
// Marketing Campaign Types
export interface Campaign {
id: string;
name: string;
type: 'email' | 'sms' | 'direct-mail' | 'social';
status: 'draft' | 'scheduled' | 'active' | 'completed' | 'cancelled';
startDate: string;
endDate?: string;
targetAudience: string[];
message: string;
sentCount: number;
deliveredCount: number;
openedCount: number;
clickedCount: number;
convertedCount: number;
roi?: number;
createdAt: string;
updatedAt: string;
}

View File

@ -0,0 +1,49 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function calendarApp() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<any[]>([]);
useEffect(() => {
setTimeout(() => {
setData([
{ id: '1', name: 'Sample Item 1', status: 'active' },
{ id: '2', name: 'Sample Item 2', status: 'active' },
{ id: '3', name: 'Sample Item 3', status: 'pending' },
]);
setLoading(false);
}, 800);
}, []);
if (loading) {
return (
<div className="app">
<div className="loading">Loading calendar view...</div>
</div>
);
}
return (
<div className="app">
<header className="header">
<h1>Calendar View</h1>
<button className="btn-primary">+ Add New</button>
</header>
<div className="content">
<div className="section">
<h2>Calendar view of appointments</h2>
<div className="data-grid">
{data.map((item) => (
<div key={item.id} className="data-card">
<h3>{item.name}</h3>
<span className={`badge badge-${item.status}`}>{item.status}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar View - FieldEdge</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,119 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f1419;
color: #e8eaed;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
font-weight: 600;
}
.btn-primary {
background: #4c9aff;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: #3d8aef;
}
.loading {
text-align: center;
padding: 4rem;
color: #9aa0a6;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section {
background: #1e2732;
border: 1px solid #2d3748;
border-radius: 8px;
padding: 1.5rem;
}
.section h2 {
font-size: 1.125rem;
margin-bottom: 1rem;
color: #e8eaed;
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.data-card {
background: #131920;
border: 1px solid #2d3748;
border-radius: 6px;
padding: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.2s;
}
.data-card:hover {
border-color: #4c9aff;
}
.data-card h3 {
font-size: 1rem;
font-weight: 500;
}
.badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-active {
background: #1a4d2e;
color: #4ade80;
}
.badge-pending {
background: #4a3810;
color: #fbbf24;
}
.badge-completed {
background: #1e3a8a;
color: #60a5fa;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: '../../../dist/ui/calendar',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,49 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function customersApp() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<any[]>([]);
useEffect(() => {
setTimeout(() => {
setData([
{ id: '1', name: 'Sample Item 1', status: 'active' },
{ id: '2', name: 'Sample Item 2', status: 'active' },
{ id: '3', name: 'Sample Item 3', status: 'pending' },
]);
setLoading(false);
}, 800);
}, []);
if (loading) {
return (
<div className="app">
<div className="loading">Loading customer management...</div>
</div>
);
}
return (
<div className="app">
<header className="header">
<h1>Customer Management</h1>
<button className="btn-primary">+ Add New</button>
</header>
<div className="content">
<div className="section">
<h2>Browse and manage customers</h2>
<div className="data-grid">
{data.map((item) => (
<div key={item.id} className="data-card">
<h3>{item.name}</h3>
<span className={`badge badge-${item.status}`}>{item.status}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Customer Management - FieldEdge</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,119 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f1419;
color: #e8eaed;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
font-weight: 600;
}
.btn-primary {
background: #4c9aff;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: #3d8aef;
}
.loading {
text-align: center;
padding: 4rem;
color: #9aa0a6;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section {
background: #1e2732;
border: 1px solid #2d3748;
border-radius: 8px;
padding: 1.5rem;
}
.section h2 {
font-size: 1.125rem;
margin-bottom: 1rem;
color: #e8eaed;
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.data-card {
background: #131920;
border: 1px solid #2d3748;
border-radius: 6px;
padding: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.2s;
}
.data-card:hover {
border-color: #4c9aff;
}
.data-card h3 {
font-size: 1rem;
font-weight: 500;
}
.badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-active {
background: #1a4d2e;
color: #4ade80;
}
.badge-pending {
background: #4a3810;
color: #fbbf24;
}
.badge-completed {
background: #1e3a8a;
color: #60a5fa;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: '../../../dist/ui/customers',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,112 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface DashboardMetrics {
totalJobs: number;
activeJobs: number;
completedToday: number;
pendingInvoices: number;
revenue: { today: number; month: number };
technicians: { active: number; total: number };
}
export default function Dashboard() {
const [metrics, setMetrics] = useState<DashboardMetrics>({
totalJobs: 0,
activeJobs: 0,
completedToday: 0,
pendingInvoices: 0,
revenue: { today: 0, month: 0 },
technicians: { active: 0, total: 0 },
});
const [loading, setLoading] = useState(true);
useEffect(() => {
// Simulate loading data
setTimeout(() => {
setMetrics({
totalJobs: 147,
activeJobs: 23,
completedToday: 8,
pendingInvoices: 15,
revenue: { today: 4250, month: 87450 },
technicians: { active: 12, total: 15 },
});
setLoading(false);
}, 1000);
}, []);
if (loading) {
return (
<div className="app">
<div className="loading">Loading dashboard...</div>
</div>
);
}
return (
<div className="app">
<header className="header">
<h1>FieldEdge Dashboard</h1>
<div className="date">{new Date().toLocaleDateString()}</div>
</header>
<div className="metrics-grid">
<div className="metric-card">
<div className="metric-label">Total Jobs</div>
<div className="metric-value">{metrics.totalJobs}</div>
</div>
<div className="metric-card">
<div className="metric-label">Active Jobs</div>
<div className="metric-value">{metrics.activeJobs}</div>
</div>
<div className="metric-card">
<div className="metric-label">Completed Today</div>
<div className="metric-value">{metrics.completedToday}</div>
</div>
<div className="metric-card">
<div className="metric-label">Pending Invoices</div>
<div className="metric-value">{metrics.pendingInvoices}</div>
</div>
<div className="metric-card">
<div className="metric-label">Today's Revenue</div>
<div className="metric-value">${metrics.revenue.today.toLocaleString()}</div>
</div>
<div className="metric-card">
<div className="metric-label">Month Revenue</div>
<div className="metric-value">${metrics.revenue.month.toLocaleString()}</div>
</div>
<div className="metric-card">
<div className="metric-label">Active Technicians</div>
<div className="metric-value">
{metrics.technicians.active} / {metrics.technicians.total}
</div>
</div>
</div>
<div className="section">
<h2>Recent Activity</h2>
<div className="activity-list">
<div className="activity-item">
<span className="activity-time">10:30 AM</span>
<span className="activity-text">Job #1234 completed by John Smith</span>
</div>
<div className="activity-item">
<span className="activity-time">09:45 AM</span>
<span className="activity-text">New job created for ACME Corp</span>
</div>
<div className="activity-item">
<span className="activity-time">09:15 AM</span>
<span className="activity-text">Invoice #INV-5678 paid - $1,250.00</span>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FieldEdge Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,104 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f1419;
color: #e8eaed;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
font-weight: 600;
}
.date {
color: #9aa0a6;
font-size: 0.95rem;
}
.loading {
text-align: center;
padding: 4rem;
color: #9aa0a6;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.metric-card {
background: #1e2732;
border: 1px solid #2d3748;
border-radius: 8px;
padding: 1.5rem;
}
.metric-label {
color: #9aa0a6;
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.metric-value {
font-size: 2rem;
font-weight: 600;
color: #4c9aff;
}
.section {
background: #1e2732;
border: 1px solid #2d3748;
border-radius: 8px;
padding: 1.5rem;
}
.section h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
color: #e8eaed;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.activity-item {
display: flex;
gap: 1rem;
padding: 0.75rem;
background: #131920;
border-radius: 6px;
}
.activity-time {
color: #9aa0a6;
font-size: 0.875rem;
min-width: 80px;
}
.activity-text {
color: #e8eaed;
font-size: 0.875rem;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: '../../../dist/ui/dashboard',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,49 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function equipmentApp() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<any[]>([]);
useEffect(() => {
setTimeout(() => {
setData([
{ id: '1', name: 'Sample Item 1', status: 'active' },
{ id: '2', name: 'Sample Item 2', status: 'active' },
{ id: '3', name: 'Sample Item 3', status: 'pending' },
]);
setLoading(false);
}, 800);
}, []);
if (loading) {
return (
<div className="app">
<div className="loading">Loading equipment management...</div>
</div>
);
}
return (
<div className="app">
<header className="header">
<h1>Equipment Management</h1>
<button className="btn-primary">+ Add New</button>
</header>
<div className="content">
<div className="section">
<h2>Track customer equipment</h2>
<div className="data-grid">
{data.map((item) => (
<div key={item.id} className="data-card">
<h3>{item.name}</h3>
<span className={`badge badge-${item.status}`}>{item.status}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Equipment Management - FieldEdge</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,119 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f1419;
color: #e8eaed;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
font-weight: 600;
}
.btn-primary {
background: #4c9aff;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: #3d8aef;
}
.loading {
text-align: center;
padding: 4rem;
color: #9aa0a6;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section {
background: #1e2732;
border: 1px solid #2d3748;
border-radius: 8px;
padding: 1.5rem;
}
.section h2 {
font-size: 1.125rem;
margin-bottom: 1rem;
color: #e8eaed;
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.data-card {
background: #131920;
border: 1px solid #2d3748;
border-radius: 6px;
padding: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.2s;
}
.data-card:hover {
border-color: #4c9aff;
}
.data-card h3 {
font-size: 1rem;
font-weight: 500;
}
.badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-active {
background: #1a4d2e;
color: #4ade80;
}
.badge-pending {
background: #4a3810;
color: #fbbf24;
}
.badge-completed {
background: #1e3a8a;
color: #60a5fa;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: '../../../dist/ui/equipment',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,49 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function estimatesApp() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<any[]>([]);
useEffect(() => {
setTimeout(() => {
setData([
{ id: '1', name: 'Sample Item 1', status: 'active' },
{ id: '2', name: 'Sample Item 2', status: 'active' },
{ id: '3', name: 'Sample Item 3', status: 'pending' },
]);
setLoading(false);
}, 800);
}, []);
if (loading) {
return (
<div className="app">
<div className="loading">Loading estimate management...</div>
</div>
);
}
return (
<div className="app">
<header className="header">
<h1>Estimate Management</h1>
<button className="btn-primary">+ Add New</button>
</header>
<div className="content">
<div className="section">
<h2>Create and manage estimates/quotes</h2>
<div className="data-grid">
{data.map((item) => (
<div key={item.id} className="data-card">
<h3>{item.name}</h3>
<span className={`badge badge-${item.status}`}>{item.status}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Estimate Management - FieldEdge</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View File

@ -0,0 +1,119 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #0f1419;
color: #e8eaed;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
font-weight: 600;
}
.btn-primary {
background: #4c9aff;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: #3d8aef;
}
.loading {
text-align: center;
padding: 4rem;
color: #9aa0a6;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section {
background: #1e2732;
border: 1px solid #2d3748;
border-radius: 8px;
padding: 1.5rem;
}
.section h2 {
font-size: 1.125rem;
margin-bottom: 1rem;
color: #e8eaed;
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.data-card {
background: #131920;
border: 1px solid #2d3748;
border-radius: 6px;
padding: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.2s;
}
.data-card:hover {
border-color: #4c9aff;
}
.data-card h3 {
font-size: 1rem;
font-weight: 500;
}
.badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-active {
background: #1a4d2e;
color: #4ade80;
}
.badge-pending {
background: #4a3810;
color: #fbbf24;
}
.badge-completed {
background: #1e3a8a;
color: #60a5fa;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: '../../../dist/ui/estimates',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,49 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function inventoryApp() {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<any[]>([]);
useEffect(() => {
setTimeout(() => {
setData([
{ id: '1', name: 'Sample Item 1', status: 'active' },
{ id: '2', name: 'Sample Item 2', status: 'active' },
{ id: '3', name: 'Sample Item 3', status: 'pending' },
]);
setLoading(false);
}, 800);
}, []);
if (loading) {
return (
<div className="app">
<div className="loading">Loading inventory management...</div>
</div>
);
}
return (
<div className="app">
<header className="header">
<h1>Inventory Management</h1>
<button className="btn-primary">+ Add New</button>
</header>
<div className="content">
<div className="section">
<h2>Manage parts and equipment inventory</h2>
<div className="data-grid">
{data.map((item) => (
<div key={item.id} className="data-card">
<h3>{item.name}</h3>
<span className={`badge badge-${item.status}`}>{item.status}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventory Management - FieldEdge</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More