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:
parent
ec4a7475d9
commit
601224bf70
@ -1,20 +1,31 @@
|
||||
{
|
||||
"name": "mcp-server-close",
|
||||
"name": "@mcpengine/close-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Complete Close CRM MCP server with 60+ tools and 22 apps",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/main.js",
|
||||
"bin": {
|
||||
"close-mcp": "./dist/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts"
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/main.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"close",
|
||||
"crm",
|
||||
"sales"
|
||||
],
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||
"zod": "^3.22.4"
|
||||
"@modelcontextprotocol/sdk": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
68
servers/close/src/apps/activity-feed.ts
Normal file
68
servers/close/src/apps/activity-feed.ts
Normal 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();
|
||||
}
|
||||
65
servers/close/src/apps/activity-timeline.ts
Normal file
65
servers/close/src/apps/activity-timeline.ts
Normal 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();
|
||||
}
|
||||
75
servers/close/src/apps/bulk-actions.ts
Normal file
75
servers/close/src/apps/bulk-actions.ts
Normal 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();
|
||||
}
|
||||
89
servers/close/src/apps/call-log.ts
Normal file
89
servers/close/src/apps/call-log.ts
Normal 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();
|
||||
}
|
||||
106
servers/close/src/apps/contact-detail.ts
Normal file
106
servers/close/src/apps/contact-detail.ts
Normal 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();
|
||||
}
|
||||
68
servers/close/src/apps/custom-fields-manager.ts
Normal file
68
servers/close/src/apps/custom-fields-manager.ts
Normal 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();
|
||||
}
|
||||
95
servers/close/src/apps/email-log.ts
Normal file
95
servers/close/src/apps/email-log.ts
Normal 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();
|
||||
}
|
||||
102
servers/close/src/apps/lead-dashboard.ts
Normal file
102
servers/close/src/apps/lead-dashboard.ts
Normal 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();
|
||||
}
|
||||
109
servers/close/src/apps/lead-detail.ts
Normal file
109
servers/close/src/apps/lead-detail.ts
Normal 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();
|
||||
}
|
||||
49
servers/close/src/apps/lead-grid.ts
Normal file
49
servers/close/src/apps/lead-grid.ts
Normal 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();
|
||||
}
|
||||
92
servers/close/src/apps/opportunity-dashboard.ts
Normal file
92
servers/close/src/apps/opportunity-dashboard.ts
Normal 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();
|
||||
}
|
||||
96
servers/close/src/apps/opportunity-detail.ts
Normal file
96
servers/close/src/apps/opportunity-detail.ts
Normal 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();
|
||||
}
|
||||
70
servers/close/src/apps/pipeline-funnel.ts
Normal file
70
servers/close/src/apps/pipeline-funnel.ts
Normal 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();
|
||||
}
|
||||
74
servers/close/src/apps/pipeline-kanban.ts
Normal file
74
servers/close/src/apps/pipeline-kanban.ts
Normal 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();
|
||||
}
|
||||
99
servers/close/src/apps/report-builder.ts
Normal file
99
servers/close/src/apps/report-builder.ts
Normal 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();
|
||||
}
|
||||
97
servers/close/src/apps/revenue-dashboard.ts
Normal file
97
servers/close/src/apps/revenue-dashboard.ts
Normal 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();
|
||||
}
|
||||
73
servers/close/src/apps/search-results.ts
Normal file
73
servers/close/src/apps/search-results.ts
Normal 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();
|
||||
}
|
||||
64
servers/close/src/apps/sequence-dashboard.ts
Normal file
64
servers/close/src/apps/sequence-dashboard.ts
Normal 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();
|
||||
}
|
||||
93
servers/close/src/apps/sequence-detail.ts
Normal file
93
servers/close/src/apps/sequence-detail.ts
Normal 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();
|
||||
}
|
||||
76
servers/close/src/apps/smart-view-runner.ts
Normal file
76
servers/close/src/apps/smart-view-runner.ts
Normal 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();
|
||||
}
|
||||
121
servers/close/src/apps/task-manager.ts
Normal file
121
servers/close/src/apps/task-manager.ts
Normal 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();
|
||||
}
|
||||
76
servers/close/src/apps/user-stats.ts
Normal file
76
servers/close/src/apps/user-stats.ts
Normal 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();
|
||||
}
|
||||
183
servers/close/src/client/close-client.ts
Normal file
183
servers/close/src/client/close-client.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
396
servers/close/src/tools/activities-tools.ts
Normal file
396
servers/close/src/tools/activities-tools.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
204
servers/close/src/tools/bulk-tools.ts
Normal file
204
servers/close/src/tools/bulk-tools.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
211
servers/close/src/tools/contacts-tools.ts
Normal file
211
servers/close/src/tools/contacts-tools.ts
Normal 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`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
212
servers/close/src/tools/custom-fields-tools.ts
Normal file
212
servers/close/src/tools/custom-fields-tools.ts
Normal 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`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
301
servers/close/src/tools/leads-tools.ts
Normal file
301
servers/close/src/tools/leads-tools.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
247
servers/close/src/tools/opportunities-tools.ts
Normal file
247
servers/close/src/tools/opportunities-tools.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
166
servers/close/src/tools/pipelines-tools.ts
Normal file
166
servers/close/src/tools/pipelines-tools.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
248
servers/close/src/tools/reporting-tools.ts
Normal file
248
servers/close/src/tools/reporting-tools.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
188
servers/close/src/tools/sequences-tools.ts
Normal file
188
servers/close/src/tools/sequences-tools.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
159
servers/close/src/tools/smart-views-tools.ts
Normal file
159
servers/close/src/tools/smart-views-tools.ts
Normal 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`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
230
servers/close/src/tools/tasks-tools.ts
Normal file
230
servers/close/src/tools/tasks-tools.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
84
servers/close/src/tools/users-tools.ts
Normal file
84
servers/close/src/tools/users-tools.ts
Normal 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),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
323
servers/close/src/types/index.ts
Normal file
323
servers/close/src/types/index.ts
Normal 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[]>;
|
||||
}
|
||||
@ -1,14 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
@ -1,231 +1,169 @@
|
||||
# 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
|
||||
|
||||
### 45+ Tools Across 10 Categories
|
||||
### 87+ Tools Across 13 Domains
|
||||
|
||||
#### Jobs Management (9 tools)
|
||||
- `fieldedge_jobs_list` - List and filter jobs
|
||||
- `fieldedge_jobs_get` - Get job details
|
||||
- `fieldedge_jobs_create` - Create new job
|
||||
- `fieldedge_jobs_update` - Update job
|
||||
- `fieldedge_jobs_complete` - Mark job complete
|
||||
- `fieldedge_jobs_cancel` - Cancel job
|
||||
- `fieldedge_jobs_line_items_list` - List job line items
|
||||
- `fieldedge_jobs_line_items_add` - Add line item to job
|
||||
- `fieldedge_jobs_equipment_list` - List equipment on job
|
||||
- **Customer Management** (10 tools): Create, update, search customers, manage balances, view history
|
||||
- **Job Management** (9 tools): Full CRUD operations, start/complete/cancel jobs, assign technicians
|
||||
- **Invoice Management** (9 tools): Create, send, void invoices, record payments, generate PDFs
|
||||
- **Estimate Management** (8 tools): Create quotes, send to customers, approve, convert to invoices
|
||||
- **Equipment Management** (7 tools): Track equipment, service history, schedule maintenance
|
||||
- **Technician Management** (9 tools): Manage technicians, schedules, availability, time tracking
|
||||
- **Scheduling & Dispatch** (8 tools): Create appointments, dispatch board, route optimization
|
||||
- **Inventory Management** (7 tools): Track parts, adjust quantities, low stock alerts
|
||||
- **Payment Management** (5 tools): Process payments, refunds, payment history
|
||||
- **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)
|
||||
- `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
|
||||
### 16 React MCP Apps
|
||||
|
||||
#### Invoice Management (6 tools)
|
||||
- `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
|
||||
Modern dark-themed React applications built with Vite:
|
||||
|
||||
#### Estimate Management (6 tools)
|
||||
- `fieldedge_estimates_list` - List estimates
|
||||
- `fieldedge_estimates_get` - Get estimate details
|
||||
- `fieldedge_estimates_create` - Create estimate
|
||||
- `fieldedge_estimates_update` - Update estimate
|
||||
- `fieldedge_estimates_send` - Send estimate to customer
|
||||
- `fieldedge_estimates_approve` - Approve and convert to job
|
||||
|
||||
#### Technician Management (6 tools)
|
||||
- `fieldedge_technicians_list` - List technicians
|
||||
- `fieldedge_technicians_get` - Get technician details
|
||||
- `fieldedge_technicians_create` - Create technician
|
||||
- `fieldedge_technicians_update` - Update technician
|
||||
- `fieldedge_technicians_performance_get` - Get performance metrics
|
||||
- `fieldedge_technicians_time_entries_list` - List time entries
|
||||
|
||||
#### 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
|
||||
1. **Dashboard** - Key metrics and recent activity
|
||||
2. **Customer Management** - Browse and manage customers
|
||||
3. **Job Management** - View and manage jobs/work orders
|
||||
4. **Scheduling & Dispatch** - Dispatch board and appointment scheduling
|
||||
5. **Invoice Management** - Create and manage invoices
|
||||
6. **Estimate Management** - Create and manage quotes
|
||||
7. **Technician Management** - Manage technicians and schedules
|
||||
8. **Equipment Management** - Track customer equipment
|
||||
9. **Inventory Management** - Parts and equipment inventory
|
||||
10. **Payment Management** - Process payments
|
||||
11. **Service Agreements** - Maintenance contracts
|
||||
12. **Reports & Analytics** - Business reports
|
||||
13. **Task Management** - Follow-ups and to-dos
|
||||
14. **Calendar View** - Appointment calendar
|
||||
15. **Map View** - Geographic view of jobs
|
||||
16. **Price Book** - Service and part pricing
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
Create a `.env` file with your FieldEdge credentials:
|
||||
|
||||
```bash
|
||||
export FIELDEDGE_API_KEY="your_api_key_here"
|
||||
export FIELDEDGE_BASE_URL="https://api.fieldedge.com/v2" # Optional, defaults to production
|
||||
```env
|
||||
FIELDEDGE_API_KEY=your_api_key_here
|
||||
FIELDEDGE_API_URL=https://api.fieldedge.com/v1
|
||||
FIELDEDGE_COMPANY_ID=your_company_id
|
||||
FIELDEDGE_TIMEOUT=30000
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### With Claude Desktop
|
||||
### As MCP Server
|
||||
|
||||
Add to your `claude_desktop_config.json`:
|
||||
Add to your MCP settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fieldedge": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/fieldedge/dist/main.js"],
|
||||
"command": "npx",
|
||||
"args": ["-y", "@mcpengine/fieldedge-mcp-server"],
|
||||
"env": {
|
||||
"FIELDEDGE_API_KEY": "your_api_key_here"
|
||||
"FIELDEDGE_API_KEY": "your_api_key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Standalone
|
||||
### Development
|
||||
|
||||
```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:**
|
||||
- "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"
|
||||
The server provides comprehensive coverage of the FieldEdge API:
|
||||
|
||||
**Customers:**
|
||||
- "Find all commercial customers in Chicago"
|
||||
- "Show me customer details for account C123"
|
||||
- "List all equipment for customer C456"
|
||||
|
||||
**Invoices:**
|
||||
- "Show me all overdue invoices"
|
||||
- "Create an invoice for job J789"
|
||||
- "Add a $500 payment to invoice INV-123"
|
||||
|
||||
**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"
|
||||
- Customer and contact management
|
||||
- Job/work order lifecycle management
|
||||
- Scheduling and dispatch operations
|
||||
- Invoicing and payment processing
|
||||
- Estimate/quote generation
|
||||
- Equipment and asset tracking
|
||||
- Inventory management
|
||||
- Technician management and time tracking
|
||||
- Service agreement management
|
||||
- Business reporting and analytics
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── client.ts # API client with auth, pagination, error handling
|
||||
├── types.ts # TypeScript type definitions
|
||||
├── tools/ # Tool implementations
|
||||
│ ├── jobs-tools.ts
|
||||
│ ├── customers-tools.ts
|
||||
│ ├── invoices-tools.ts
|
||||
│ ├── estimates-tools.ts
|
||||
│ ├── technicians-tools.ts
|
||||
│ ├── dispatch-tools.ts
|
||||
│ ├── equipment-tools.ts
|
||||
│ ├── inventory-tools.ts
|
||||
│ ├── agreements-tools.ts
|
||||
│ └── reporting-tools.ts
|
||||
├── apps/ # MCP app implementations
|
||||
│ └── index.ts
|
||||
├── server.ts # MCP server setup
|
||||
└── main.ts # Entry point
|
||||
├── clients/
|
||||
│ └── fieldedge.ts # API client with auth, pagination, error handling
|
||||
├── types/
|
||||
│ └── index.ts # TypeScript type definitions
|
||||
├── tools/
|
||||
│ ├── customers.ts # Customer management tools
|
||||
│ ├── jobs.ts # Job management tools
|
||||
│ ├── invoices.ts # Invoice management tools
|
||||
│ ├── estimates.ts # Estimate management tools
|
||||
│ ├── equipment.ts # Equipment management tools
|
||||
│ ├── technicians.ts # Technician management tools
|
||||
│ ├── scheduling.ts # Scheduling and dispatch tools
|
||||
│ ├── inventory.ts # Inventory management tools
|
||||
│ ├── payments.ts # Payment management tools
|
||||
│ ├── reporting.ts # Reporting and analytics tools
|
||||
│ ├── locations.ts # Location management tools
|
||||
│ ├── 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
|
||||
- **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
|
||||
The client includes comprehensive error handling:
|
||||
|
||||
## Development
|
||||
- Authentication errors (401)
|
||||
- Permission errors (403)
|
||||
- Not found errors (404)
|
||||
- Rate limiting (429)
|
||||
- Server errors (5xx)
|
||||
- Network errors
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
## Rate Limiting
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
Automatic rate limit tracking and retry logic included. The client monitors rate limit headers and automatically waits when limits are approached.
|
||||
|
||||
# Development with watch mode
|
||||
npm run build -- --watch
|
||||
## TypeScript Support
|
||||
|
||||
# Run tests (if implemented)
|
||||
npm test
|
||||
```
|
||||
Full TypeScript support with comprehensive type definitions for:
|
||||
|
||||
- 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
|
||||
|
||||
@ -233,6 +171,4 @@ MIT
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- FieldEdge API documentation: https://developer.fieldedge.com
|
||||
- MCP Protocol: https://modelcontextprotocol.io
|
||||
For API access and documentation, visit [docs.api.fieldedge.com](https://docs.api.fieldedge.com)
|
||||
|
||||
@ -1,31 +1,42 @@
|
||||
{
|
||||
"name": "@mcpengine/fieldedge",
|
||||
"name": "@mcpengine/fieldedge-mcp-server",
|
||||
"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",
|
||||
"main": "dist/main.js",
|
||||
"bin": {
|
||||
"fieldedge-mcp": "./dist/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"fieldedge",
|
||||
"field-service",
|
||||
"hvac",
|
||||
"dispatch",
|
||||
"service-management"
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"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": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4"
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"axios": "^1.7.9",
|
||||
"dotenv": "^16.4.7",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,45 +1,41 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build script for React UI apps
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const uiDir = join(__dirname, '../src/ui');
|
||||
|
||||
const reactAppDir = join(__dirname, '..', 'src', 'ui', 'react-app');
|
||||
console.log('Building React apps...\n');
|
||||
|
||||
try {
|
||||
const apps = readdirSync(reactAppDir).filter(file => {
|
||||
const fullPath = join(reactAppDir, file);
|
||||
return statSync(fullPath).isDirectory();
|
||||
});
|
||||
const apps = readdirSync(uiDir).filter((file) => {
|
||||
const fullPath = join(uiDir, file);
|
||||
return statSync(fullPath).isDirectory();
|
||||
});
|
||||
|
||||
console.log(`Found ${apps.length} React apps to build`);
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const app of apps) {
|
||||
const appPath = join(reactAppDir, app);
|
||||
console.log(`Building ${app}...`);
|
||||
|
||||
try {
|
||||
execSync('npx vite build', {
|
||||
cwd: appPath,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
console.log(`✓ ${app} built successfully`);
|
||||
} catch (error) {
|
||||
console.error(`✗ Failed to build ${app}`);
|
||||
}
|
||||
for (const app of apps) {
|
||||
const appPath = join(uiDir, app);
|
||||
console.log(`Building ${app}...`);
|
||||
|
||||
try {
|
||||
execSync('npx vite build', {
|
||||
cwd: appPath,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to build ${app}`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('UI build complete');
|
||||
} catch (error) {
|
||||
console.error('Build failed:', error.message);
|
||||
console.log(`\n✅ Successfully built ${successCount} apps`);
|
||||
if (failCount > 0) {
|
||||
console.log(`❌ Failed to build ${failCount} apps`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
256
servers/fieldedge/scripts/generate-apps.js
Normal file
256
servers/fieldedge/scripts/generate-apps.js
Normal 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!`);
|
||||
@ -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...',
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
290
servers/fieldedge/src/clients/fieldedge.ts
Normal file
290
servers/fieldedge/src/clients/fieldedge.ts
Normal 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;
|
||||
}
|
||||
@ -1,22 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* FieldEdge MCP Server - Main Entry Point
|
||||
* FieldEdge MCP Server Entry Point
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
import { FieldEdgeServer } from './server.js';
|
||||
import { initializeFieldEdgeClient } from './clients/fieldedge.js';
|
||||
|
||||
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 {
|
||||
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();
|
||||
} catch (error) {
|
||||
console.error('Fatal error:', error);
|
||||
|
||||
@ -7,56 +7,88 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
ErrorCode,
|
||||
McpError,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { initializeFieldEdgeClient } from './clients/fieldedge.js';
|
||||
|
||||
import { FieldEdgeClient } from './client.js';
|
||||
import { createJobsTools } from './tools/jobs-tools.js';
|
||||
import { createCustomersTools } from './tools/customers-tools.js';
|
||||
import { createInvoicesTools } from './tools/invoices-tools.js';
|
||||
import { createEstimatesTools } from './tools/estimates-tools.js';
|
||||
import { createTechniciansTools } from './tools/technicians-tools.js';
|
||||
import { createDispatchTools } from './tools/dispatch-tools.js';
|
||||
import { createEquipmentTools } from './tools/equipment-tools.js';
|
||||
import { createInventoryTools } from './tools/inventory-tools.js';
|
||||
import { createAgreementsTools } from './tools/agreements-tools.js';
|
||||
import { createReportingTools } from './tools/reporting-tools.js';
|
||||
import { createApps } from './apps/index.js';
|
||||
// Import tool definitions and handlers
|
||||
import { customerTools, handleCustomerTool } from './tools/customers.js';
|
||||
import { jobTools, handleJobTool } from './tools/jobs.js';
|
||||
import { invoiceTools, handleInvoiceTool } from './tools/invoices.js';
|
||||
import { estimateTools, handleEstimateTool } from './tools/estimates.js';
|
||||
import { equipmentTools, handleEquipmentTool } from './tools/equipment.js';
|
||||
import { technicianTools, handleTechnicianTool } from './tools/technicians.js';
|
||||
import { schedulingTools, handleSchedulingTool } from './tools/scheduling.js';
|
||||
import { inventoryTools, handleInventoryTool } from './tools/inventory.js';
|
||||
import { paymentTools, handlePaymentTool } from './tools/payments.js';
|
||||
import { reportingTools, handleReportingTool } from './tools/reporting.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 {
|
||||
private server: Server;
|
||||
private client: FieldEdgeClient;
|
||||
private tools: any[];
|
||||
private apps: any[];
|
||||
|
||||
constructor(apiKey: string, baseUrl?: string) {
|
||||
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
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'fieldedge',
|
||||
name: 'fieldedge-mcp-server',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -68,38 +100,174 @@ export class FieldEdgeServer {
|
||||
// List available tools
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: this.tools.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
})),
|
||||
tools: ALL_TOOLS,
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const tool = this.tools.find((t) => t.name === request.params.name);
|
||||
|
||||
if (!tool) {
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
}
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
const result = await tool.handler(request.params.arguments || {});
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
const handler = TOOL_HANDLERS[name];
|
||||
if (!handler) {
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
`Unknown tool: ${name}`
|
||||
);
|
||||
}
|
||||
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}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
237
servers/fieldedge/src/tools/customers.ts
Normal file
237
servers/fieldedge/src/tools/customers.ts
Normal 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: [],
|
||||
};
|
||||
}
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
158
servers/fieldedge/src/tools/equipment.ts
Normal file
158
servers/fieldedge/src/tools/equipment.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
173
servers/fieldedge/src/tools/estimates.ts
Normal file
173
servers/fieldedge/src/tools/estimates.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
147
servers/fieldedge/src/tools/inventory.ts
Normal file
147
servers/fieldedge/src/tools/inventory.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
201
servers/fieldedge/src/tools/invoices.ts
Normal file
201
servers/fieldedge/src/tools/invoices.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
200
servers/fieldedge/src/tools/jobs.ts
Normal file
200
servers/fieldedge/src/tools/jobs.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
112
servers/fieldedge/src/tools/locations.ts
Normal file
112
servers/fieldedge/src/tools/locations.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
108
servers/fieldedge/src/tools/payments.ts
Normal file
108
servers/fieldedge/src/tools/payments.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
140
servers/fieldedge/src/tools/reporting.ts
Normal file
140
servers/fieldedge/src/tools/reporting.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
175
servers/fieldedge/src/tools/scheduling.ts
Normal file
175
servers/fieldedge/src/tools/scheduling.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
139
servers/fieldedge/src/tools/service-agreements.ts
Normal file
139
servers/fieldedge/src/tools/service-agreements.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
125
servers/fieldedge/src/tools/tasks.ts
Normal file
125
servers/fieldedge/src/tools/tasks.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
184
servers/fieldedge/src/tools/technicians.ts
Normal file
184
servers/fieldedge/src/tools/technicians.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
602
servers/fieldedge/src/types/index.ts
Normal file
602
servers/fieldedge/src/types/index.ts
Normal 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;
|
||||
}
|
||||
49
servers/fieldedge/src/ui/calendar/App.tsx
Normal file
49
servers/fieldedge/src/ui/calendar/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/fieldedge/src/ui/calendar/index.html
Normal file
12
servers/fieldedge/src/ui/calendar/index.html
Normal 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>
|
||||
9
servers/fieldedge/src/ui/calendar/main.tsx
Normal file
9
servers/fieldedge/src/ui/calendar/main.tsx
Normal 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>
|
||||
);
|
||||
119
servers/fieldedge/src/ui/calendar/styles.css
Normal file
119
servers/fieldedge/src/ui/calendar/styles.css
Normal 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;
|
||||
}
|
||||
10
servers/fieldedge/src/ui/calendar/vite.config.ts
Normal file
10
servers/fieldedge/src/ui/calendar/vite.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
49
servers/fieldedge/src/ui/customers/App.tsx
Normal file
49
servers/fieldedge/src/ui/customers/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/fieldedge/src/ui/customers/index.html
Normal file
12
servers/fieldedge/src/ui/customers/index.html
Normal 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>
|
||||
9
servers/fieldedge/src/ui/customers/main.tsx
Normal file
9
servers/fieldedge/src/ui/customers/main.tsx
Normal 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>
|
||||
);
|
||||
119
servers/fieldedge/src/ui/customers/styles.css
Normal file
119
servers/fieldedge/src/ui/customers/styles.css
Normal 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;
|
||||
}
|
||||
10
servers/fieldedge/src/ui/customers/vite.config.ts
Normal file
10
servers/fieldedge/src/ui/customers/vite.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
112
servers/fieldedge/src/ui/dashboard/App.tsx
Normal file
112
servers/fieldedge/src/ui/dashboard/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/fieldedge/src/ui/dashboard/index.html
Normal file
12
servers/fieldedge/src/ui/dashboard/index.html
Normal 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>
|
||||
9
servers/fieldedge/src/ui/dashboard/main.tsx
Normal file
9
servers/fieldedge/src/ui/dashboard/main.tsx
Normal 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>
|
||||
);
|
||||
104
servers/fieldedge/src/ui/dashboard/styles.css
Normal file
104
servers/fieldedge/src/ui/dashboard/styles.css
Normal 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;
|
||||
}
|
||||
10
servers/fieldedge/src/ui/dashboard/vite.config.ts
Normal file
10
servers/fieldedge/src/ui/dashboard/vite.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
49
servers/fieldedge/src/ui/equipment/App.tsx
Normal file
49
servers/fieldedge/src/ui/equipment/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/fieldedge/src/ui/equipment/index.html
Normal file
12
servers/fieldedge/src/ui/equipment/index.html
Normal 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>
|
||||
9
servers/fieldedge/src/ui/equipment/main.tsx
Normal file
9
servers/fieldedge/src/ui/equipment/main.tsx
Normal 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>
|
||||
);
|
||||
119
servers/fieldedge/src/ui/equipment/styles.css
Normal file
119
servers/fieldedge/src/ui/equipment/styles.css
Normal 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;
|
||||
}
|
||||
10
servers/fieldedge/src/ui/equipment/vite.config.ts
Normal file
10
servers/fieldedge/src/ui/equipment/vite.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
49
servers/fieldedge/src/ui/estimates/App.tsx
Normal file
49
servers/fieldedge/src/ui/estimates/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/fieldedge/src/ui/estimates/index.html
Normal file
12
servers/fieldedge/src/ui/estimates/index.html
Normal 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>
|
||||
9
servers/fieldedge/src/ui/estimates/main.tsx
Normal file
9
servers/fieldedge/src/ui/estimates/main.tsx
Normal 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>
|
||||
);
|
||||
119
servers/fieldedge/src/ui/estimates/styles.css
Normal file
119
servers/fieldedge/src/ui/estimates/styles.css
Normal 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;
|
||||
}
|
||||
10
servers/fieldedge/src/ui/estimates/vite.config.ts
Normal file
10
servers/fieldedge/src/ui/estimates/vite.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
49
servers/fieldedge/src/ui/inventory/App.tsx
Normal file
49
servers/fieldedge/src/ui/inventory/App.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
servers/fieldedge/src/ui/inventory/index.html
Normal file
12
servers/fieldedge/src/ui/inventory/index.html
Normal 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
Loading…
x
Reference in New Issue
Block a user