655 lines
27 KiB
JavaScript
655 lines
27 KiB
JavaScript
/**
|
|
* MCP Apps Manager
|
|
* Manages rich UI components for GoHighLevel MCP Server
|
|
*/
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
/**
|
|
* MCP Apps Manager class
|
|
* Registers app tools and handles structuredContent responses
|
|
*/
|
|
// ESM equivalent of __dirname
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
// Resolve UI build path - works regardless of working directory
|
|
function getUIBuildPath() {
|
|
// When compiled, this file is at dist/apps/index.js
|
|
// UI files are at dist/app-ui/
|
|
const fromDist = path.resolve(__dirname, '..', 'app-ui');
|
|
if (fs.existsSync(fromDist)) {
|
|
return fromDist;
|
|
}
|
|
// Fallback: try process.cwd() based paths
|
|
const appUiPath = path.join(process.cwd(), 'dist', 'app-ui');
|
|
if (fs.existsSync(appUiPath)) {
|
|
return appUiPath;
|
|
}
|
|
// Default fallback
|
|
return fromDist;
|
|
}
|
|
export class MCPAppsManager {
|
|
ghlClient;
|
|
resourceHandlers = new Map();
|
|
uiBuildPath;
|
|
constructor(ghlClient) {
|
|
this.ghlClient = ghlClient;
|
|
this.uiBuildPath = getUIBuildPath();
|
|
process.stderr.write(`[MCP Apps] UI build path: ${this.uiBuildPath}\n`);
|
|
this.registerResourceHandlers();
|
|
}
|
|
/**
|
|
* Register all UI resource handlers
|
|
*/
|
|
registerResourceHandlers() {
|
|
const resources = [
|
|
// All 11 MCP Apps
|
|
{ uri: 'ui://ghl/mcp-app', file: 'mcp-app.html' },
|
|
{ uri: 'ui://ghl/pipeline-board', file: 'pipeline-board.html' },
|
|
{ uri: 'ui://ghl/quick-book', file: 'quick-book.html' },
|
|
{ uri: 'ui://ghl/opportunity-card', file: 'opportunity-card.html' },
|
|
{ uri: 'ui://ghl/contact-grid', file: 'contact-grid.html' },
|
|
{ uri: 'ui://ghl/calendar-view', file: 'calendar-view.html' },
|
|
{ uri: 'ui://ghl/invoice-preview', file: 'invoice-preview.html' },
|
|
{ uri: 'ui://ghl/campaign-stats', file: 'campaign-stats.html' },
|
|
{ uri: 'ui://ghl/agent-stats', file: 'agent-stats.html' },
|
|
{ uri: 'ui://ghl/contact-timeline', file: 'contact-timeline.html' },
|
|
{ uri: 'ui://ghl/workflow-status', file: 'workflow-status.html' },
|
|
];
|
|
for (const resource of resources) {
|
|
this.resourceHandlers.set(resource.uri, {
|
|
uri: resource.uri,
|
|
mimeType: 'text/html;profile=mcp-app',
|
|
getContent: () => this.loadUIResource(resource.file),
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Load UI resource from build directory
|
|
*/
|
|
loadUIResource(filename) {
|
|
const filePath = path.join(this.uiBuildPath, filename);
|
|
try {
|
|
return fs.readFileSync(filePath, 'utf-8');
|
|
}
|
|
catch (error) {
|
|
process.stderr.write(`[MCP Apps] UI resource not found: ${filePath}\n`);
|
|
return this.getFallbackHTML(filename);
|
|
}
|
|
}
|
|
/**
|
|
* Generate fallback HTML when UI resource is not built
|
|
*/
|
|
getFallbackHTML(filename) {
|
|
const componentName = filename.replace('.html', '');
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GHL ${componentName}</title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 20px; }
|
|
.fallback { text-align: center; color: #666; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="fallback">
|
|
<p>UI component "${componentName}" is loading...</p>
|
|
<p>Run <code>npm run build:ui</code> to build UI components.</p>
|
|
</div>
|
|
<script>
|
|
window.addEventListener('message', (e) => {
|
|
if (e.data?.type === 'mcp-app-init') {
|
|
console.log('MCP App data:', e.data.data);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`.trim();
|
|
}
|
|
/**
|
|
* Get tool definitions for all app tools
|
|
*/
|
|
getToolDefinitions() {
|
|
return [
|
|
// 1. Contact Grid - search and display contacts
|
|
{
|
|
name: 'view_contact_grid',
|
|
description: 'Display contact search results in a data grid with sorting and pagination. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
query: { type: 'string', description: 'Search query string' },
|
|
limit: { type: 'number', description: 'Maximum results (default: 25)' }
|
|
}
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/contact-grid' }
|
|
}
|
|
},
|
|
// 2. Pipeline Board - Kanban view of opportunities
|
|
{
|
|
name: 'view_pipeline_board',
|
|
description: 'Display a pipeline as an interactive Kanban board with opportunities. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
pipelineId: { type: 'string', description: 'Pipeline ID to display' }
|
|
},
|
|
required: ['pipelineId']
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/pipeline-board' }
|
|
}
|
|
},
|
|
// 3. Quick Book - appointment booking
|
|
{
|
|
name: 'view_quick_book',
|
|
description: 'Display a quick booking interface for scheduling appointments. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
calendarId: { type: 'string', description: 'Calendar ID for booking' },
|
|
contactId: { type: 'string', description: 'Optional contact ID to pre-fill' }
|
|
},
|
|
required: ['calendarId']
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/quick-book' }
|
|
}
|
|
},
|
|
// 4. Opportunity Card - single opportunity details
|
|
{
|
|
name: 'view_opportunity_card',
|
|
description: 'Display a single opportunity with details, value, and stage info. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
opportunityId: { type: 'string', description: 'Opportunity ID to display' }
|
|
},
|
|
required: ['opportunityId']
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/opportunity-card' }
|
|
}
|
|
},
|
|
// 5. Calendar View - calendar with events
|
|
{
|
|
name: 'view_calendar',
|
|
description: 'Display a calendar with events and appointments. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
calendarId: { type: 'string', description: 'Calendar ID to display' },
|
|
startDate: { type: 'string', description: 'Start date (ISO format)' },
|
|
endDate: { type: 'string', description: 'End date (ISO format)' }
|
|
},
|
|
required: ['calendarId']
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/calendar-view' }
|
|
}
|
|
},
|
|
// 6. Invoice Preview - invoice details
|
|
{
|
|
name: 'view_invoice',
|
|
description: 'Display an invoice preview with line items and payment status. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
invoiceId: { type: 'string', description: 'Invoice ID to display' }
|
|
},
|
|
required: ['invoiceId']
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/invoice-preview' }
|
|
}
|
|
},
|
|
// 7. Campaign Stats - campaign performance metrics
|
|
{
|
|
name: 'view_campaign_stats',
|
|
description: 'Display campaign statistics and performance metrics. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
campaignId: { type: 'string', description: 'Campaign ID to display stats for' }
|
|
},
|
|
required: ['campaignId']
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/campaign-stats' }
|
|
}
|
|
},
|
|
// 8. Agent Stats - agent/user performance
|
|
{
|
|
name: 'view_agent_stats',
|
|
description: 'Display agent/user performance statistics and metrics. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
userId: { type: 'string', description: 'User/Agent ID to display stats for' },
|
|
dateRange: { type: 'string', description: 'Date range (e.g., "last7days", "last30days")' }
|
|
}
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/agent-stats' }
|
|
}
|
|
},
|
|
// 9. Contact Timeline - activity history for a contact
|
|
{
|
|
name: 'view_contact_timeline',
|
|
description: 'Display a contact\'s activity timeline with all interactions. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
contactId: { type: 'string', description: 'Contact ID to display timeline for' }
|
|
},
|
|
required: ['contactId']
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/contact-timeline' }
|
|
}
|
|
},
|
|
// 10. Workflow Status - workflow execution status
|
|
{
|
|
name: 'view_workflow_status',
|
|
description: 'Display workflow execution status and history. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
workflowId: { type: 'string', description: 'Workflow ID to display status for' }
|
|
},
|
|
required: ['workflowId']
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/workflow-status' }
|
|
}
|
|
},
|
|
// 11. MCP App - generic/main dashboard
|
|
{
|
|
name: 'view_dashboard',
|
|
description: 'Display the main GHL dashboard overview. Returns a visual UI component.',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {}
|
|
},
|
|
_meta: {
|
|
ui: { resourceUri: 'ui://ghl/mcp-app' }
|
|
}
|
|
},
|
|
// 12. Update Opportunity - action tool for UI to update opportunities
|
|
{
|
|
name: 'update_opportunity',
|
|
description: 'Update an opportunity (move to stage, change value, status, etc.)',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
opportunityId: { type: 'string', description: 'Opportunity ID to update' },
|
|
pipelineStageId: { type: 'string', description: 'New stage ID (for moving)' },
|
|
name: { type: 'string', description: 'Opportunity name' },
|
|
monetaryValue: { type: 'number', description: 'Monetary value' },
|
|
status: { type: 'string', enum: ['open', 'won', 'lost', 'abandoned'], description: 'Opportunity status' }
|
|
},
|
|
required: ['opportunityId']
|
|
}
|
|
}
|
|
];
|
|
}
|
|
/**
|
|
* Get app tool names for routing
|
|
*/
|
|
getAppToolNames() {
|
|
return [
|
|
'view_contact_grid',
|
|
'view_pipeline_board',
|
|
'view_quick_book',
|
|
'view_opportunity_card',
|
|
'view_calendar',
|
|
'view_invoice',
|
|
'view_campaign_stats',
|
|
'view_agent_stats',
|
|
'view_contact_timeline',
|
|
'view_workflow_status',
|
|
'view_dashboard',
|
|
'update_opportunity'
|
|
];
|
|
}
|
|
/**
|
|
* Check if a tool is an app tool
|
|
*/
|
|
isAppTool(toolName) {
|
|
return this.getAppToolNames().includes(toolName);
|
|
}
|
|
/**
|
|
* Execute an app tool
|
|
*/
|
|
async executeTool(toolName, args) {
|
|
process.stderr.write(`[MCP Apps] Executing app tool: ${toolName}\n`);
|
|
switch (toolName) {
|
|
case 'view_contact_grid':
|
|
return await this.viewContactGrid(args.query, args.limit);
|
|
case 'view_pipeline_board':
|
|
return await this.viewPipelineBoard(args.pipelineId);
|
|
case 'view_quick_book':
|
|
return await this.viewQuickBook(args.calendarId, args.contactId);
|
|
case 'view_opportunity_card':
|
|
return await this.viewOpportunityCard(args.opportunityId);
|
|
case 'view_calendar':
|
|
return await this.viewCalendar(args.calendarId, args.startDate, args.endDate);
|
|
case 'view_invoice':
|
|
return await this.viewInvoice(args.invoiceId);
|
|
case 'view_campaign_stats':
|
|
return await this.viewCampaignStats(args.campaignId);
|
|
case 'view_agent_stats':
|
|
return await this.viewAgentStats(args.userId, args.dateRange);
|
|
case 'view_contact_timeline':
|
|
return await this.viewContactTimeline(args.contactId);
|
|
case 'view_workflow_status':
|
|
return await this.viewWorkflowStatus(args.workflowId);
|
|
case 'view_dashboard':
|
|
return await this.viewDashboard();
|
|
case 'update_opportunity':
|
|
return await this.updateOpportunity(args);
|
|
default:
|
|
throw new Error(`Unknown app tool: ${toolName}`);
|
|
}
|
|
}
|
|
/**
|
|
* View contact grid (search results)
|
|
*/
|
|
async viewContactGrid(query, limit) {
|
|
const response = await this.ghlClient.searchContacts({
|
|
locationId: this.ghlClient.getConfig().locationId,
|
|
query: query,
|
|
limit: limit || 25
|
|
});
|
|
if (!response.success) {
|
|
throw new Error(response.error?.message || 'Failed to search contacts');
|
|
}
|
|
const data = response.data;
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/contact-grid');
|
|
return this.createAppResult(`Found ${data?.contacts?.length || 0} contacts`, resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), data);
|
|
}
|
|
/**
|
|
* View pipeline board (Kanban)
|
|
*/
|
|
async viewPipelineBoard(pipelineId) {
|
|
const [pipelinesResponse, opportunitiesResponse] = await Promise.all([
|
|
this.ghlClient.getPipelines(),
|
|
this.ghlClient.searchOpportunities({
|
|
location_id: this.ghlClient.getConfig().locationId,
|
|
pipeline_id: pipelineId
|
|
})
|
|
]);
|
|
if (!pipelinesResponse.success) {
|
|
throw new Error(pipelinesResponse.error?.message || 'Failed to get pipeline');
|
|
}
|
|
const pipeline = pipelinesResponse.data?.pipelines?.find((p) => p.id === pipelineId);
|
|
const opportunities = opportunitiesResponse.data?.opportunities || [];
|
|
// Simplify opportunity data to only include fields the UI needs (reduces payload size)
|
|
const simplifiedOpportunities = opportunities.map((opp) => ({
|
|
id: opp.id,
|
|
name: opp.name || 'Untitled',
|
|
pipelineStageId: opp.pipelineStageId,
|
|
status: opp.status || 'open',
|
|
monetaryValue: opp.monetaryValue || 0,
|
|
contact: opp.contact ? {
|
|
name: opp.contact.name || 'Unknown',
|
|
email: opp.contact.email,
|
|
phone: opp.contact.phone
|
|
} : { name: 'Unknown' },
|
|
updatedAt: opp.updatedAt || opp.createdAt,
|
|
createdAt: opp.createdAt,
|
|
source: opp.source
|
|
}));
|
|
const data = {
|
|
pipeline,
|
|
opportunities: simplifiedOpportunities,
|
|
stages: pipeline?.stages || []
|
|
};
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/pipeline-board');
|
|
return this.createAppResult(`Pipeline: ${pipeline?.name || 'Unknown'} (${opportunities.length} opportunities)`, resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), data);
|
|
}
|
|
/**
|
|
* View quick book interface
|
|
*/
|
|
async viewQuickBook(calendarId, contactId) {
|
|
const [calendarResponse, contactResponse] = await Promise.all([
|
|
this.ghlClient.getCalendar(calendarId),
|
|
contactId ? this.ghlClient.getContact(contactId) : Promise.resolve({ success: true, data: null })
|
|
]);
|
|
if (!calendarResponse.success) {
|
|
throw new Error(calendarResponse.error?.message || 'Failed to get calendar');
|
|
}
|
|
const data = {
|
|
calendar: calendarResponse.data,
|
|
contact: contactResponse.data,
|
|
locationId: this.ghlClient.getConfig().locationId
|
|
};
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/quick-book');
|
|
return this.createAppResult(`Quick booking for calendar: ${calendarResponse.data?.name || calendarId}`, resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), data);
|
|
}
|
|
/**
|
|
* View opportunity card
|
|
*/
|
|
async viewOpportunityCard(opportunityId) {
|
|
const response = await this.ghlClient.getOpportunity(opportunityId);
|
|
if (!response.success) {
|
|
throw new Error(response.error?.message || 'Failed to get opportunity');
|
|
}
|
|
const opportunity = response.data;
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/opportunity-card');
|
|
return this.createAppResult(`Opportunity: ${opportunity?.name || opportunityId}`, resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), opportunity);
|
|
}
|
|
/**
|
|
* View calendar
|
|
*/
|
|
async viewCalendar(calendarId, startDate, endDate) {
|
|
const now = new Date();
|
|
const start = startDate || new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
|
|
const end = endDate || new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString();
|
|
const [calendarResponse, eventsResponse] = await Promise.all([
|
|
this.ghlClient.getCalendar(calendarId),
|
|
this.ghlClient.getCalendarEvents({
|
|
calendarId: calendarId,
|
|
startTime: start,
|
|
endTime: end,
|
|
locationId: this.ghlClient.getConfig().locationId
|
|
})
|
|
]);
|
|
if (!calendarResponse.success) {
|
|
throw new Error(calendarResponse.error?.message || 'Failed to get calendar');
|
|
}
|
|
const calendar = calendarResponse.data;
|
|
const data = {
|
|
calendar: calendarResponse.data,
|
|
events: eventsResponse.data?.events || [],
|
|
startDate: start,
|
|
endDate: end
|
|
};
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/calendar-view');
|
|
return this.createAppResult(`Calendar: ${calendar?.name || 'Unknown'} (${data.events.length} events)`, resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), data);
|
|
}
|
|
/**
|
|
* View campaign stats
|
|
*/
|
|
async viewCampaignStats(campaignId) {
|
|
// Get email campaigns
|
|
const response = await this.ghlClient.getEmailCampaigns({});
|
|
const campaigns = response.data?.schedules || [];
|
|
const campaign = campaigns.find((c) => c.id === campaignId) || { id: campaignId };
|
|
const data = {
|
|
campaign,
|
|
campaigns,
|
|
campaignId,
|
|
locationId: this.ghlClient.getConfig().locationId
|
|
};
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/campaign-stats');
|
|
return this.createAppResult(`Campaign stats: ${campaign?.name || campaignId}`, resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), data);
|
|
}
|
|
/**
|
|
* View agent stats
|
|
*/
|
|
async viewAgentStats(userId, dateRange) {
|
|
// Get location info which may include user data
|
|
const locationResponse = await this.ghlClient.getLocationById(this.ghlClient.getConfig().locationId);
|
|
const data = {
|
|
userId,
|
|
dateRange: dateRange || 'last30days',
|
|
location: locationResponse.data,
|
|
locationId: this.ghlClient.getConfig().locationId
|
|
};
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/agent-stats');
|
|
return this.createAppResult(userId ? `Agent stats: ${userId}` : 'Agent overview', resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), data);
|
|
}
|
|
/**
|
|
* View contact timeline
|
|
*/
|
|
async viewContactTimeline(contactId) {
|
|
const [contactResponse, notesResponse, tasksResponse] = await Promise.all([
|
|
this.ghlClient.getContact(contactId),
|
|
this.ghlClient.getContactNotes(contactId),
|
|
this.ghlClient.getContactTasks(contactId)
|
|
]);
|
|
if (!contactResponse.success) {
|
|
throw new Error(contactResponse.error?.message || 'Failed to get contact');
|
|
}
|
|
const contact = contactResponse.data;
|
|
const data = {
|
|
contact: contactResponse.data,
|
|
notes: notesResponse.data || [],
|
|
tasks: tasksResponse.data || []
|
|
};
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/contact-timeline');
|
|
return this.createAppResult(`Timeline for ${contact?.firstName || ''} ${contact?.lastName || ''}`, resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), data);
|
|
}
|
|
/**
|
|
* View workflow status
|
|
*/
|
|
async viewWorkflowStatus(workflowId) {
|
|
const response = await this.ghlClient.getWorkflows({
|
|
locationId: this.ghlClient.getConfig().locationId
|
|
});
|
|
const workflows = response.data?.workflows || [];
|
|
const workflow = workflows.find((w) => w.id === workflowId) || { id: workflowId };
|
|
const data = {
|
|
workflow,
|
|
workflows,
|
|
workflowId,
|
|
locationId: this.ghlClient.getConfig().locationId
|
|
};
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/workflow-status');
|
|
return this.createAppResult(`Workflow: ${workflow?.name || workflowId}`, resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), data);
|
|
}
|
|
/**
|
|
* View main dashboard
|
|
*/
|
|
async viewDashboard() {
|
|
const [contactsResponse, pipelinesResponse, calendarsResponse] = await Promise.all([
|
|
this.ghlClient.searchContacts({ locationId: this.ghlClient.getConfig().locationId, limit: 10 }),
|
|
this.ghlClient.getPipelines(),
|
|
this.ghlClient.getCalendars()
|
|
]);
|
|
const data = {
|
|
recentContacts: contactsResponse.data?.contacts || [],
|
|
pipelines: pipelinesResponse.data?.pipelines || [],
|
|
calendars: calendarsResponse.data?.calendars || [],
|
|
locationId: this.ghlClient.getConfig().locationId
|
|
};
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/mcp-app');
|
|
return this.createAppResult('GHL Dashboard Overview', resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), data);
|
|
}
|
|
/**
|
|
* View invoice
|
|
*/
|
|
async viewInvoice(invoiceId) {
|
|
const response = await this.ghlClient.getInvoice(invoiceId, {
|
|
altId: this.ghlClient.getConfig().locationId,
|
|
altType: 'location'
|
|
});
|
|
if (!response.success) {
|
|
throw new Error(response.error?.message || 'Failed to get invoice');
|
|
}
|
|
const invoice = response.data;
|
|
const resourceHandler = this.resourceHandlers.get('ui://ghl/invoice-preview');
|
|
return this.createAppResult(`Invoice #${invoice?.invoiceNumber || invoiceId} - ${invoice?.status || 'Unknown status'}`, resourceHandler.uri, resourceHandler.mimeType, resourceHandler.getContent(), invoice);
|
|
}
|
|
/**
|
|
* Update opportunity (action tool for UI)
|
|
*/
|
|
async updateOpportunity(args) {
|
|
const { opportunityId, ...updates } = args;
|
|
// Build the update payload
|
|
const updatePayload = {};
|
|
if (updates.pipelineStageId)
|
|
updatePayload.pipelineStageId = updates.pipelineStageId;
|
|
if (updates.name)
|
|
updatePayload.name = updates.name;
|
|
if (updates.monetaryValue !== undefined)
|
|
updatePayload.monetaryValue = updates.monetaryValue;
|
|
if (updates.status)
|
|
updatePayload.status = updates.status;
|
|
process.stderr.write(`[MCP Apps] Updating opportunity ${opportunityId}: ${JSON.stringify(updatePayload)}\n`);
|
|
const response = await this.ghlClient.updateOpportunity(opportunityId, updatePayload);
|
|
if (!response.success) {
|
|
throw new Error(response.error?.message || 'Failed to update opportunity');
|
|
}
|
|
const opportunity = response.data;
|
|
return {
|
|
content: [{ type: 'text', text: `Updated opportunity: ${opportunity?.name || opportunityId}` }],
|
|
structuredContent: {
|
|
success: true,
|
|
opportunity: {
|
|
id: opportunity?.id,
|
|
name: opportunity?.name,
|
|
pipelineStageId: opportunity?.pipelineStageId,
|
|
monetaryValue: opportunity?.monetaryValue,
|
|
status: opportunity?.status
|
|
}
|
|
}
|
|
};
|
|
}
|
|
/**
|
|
* Create app tool result with structuredContent
|
|
*/
|
|
createAppResult(textSummary, resourceUri, mimeType, htmlContent, data) {
|
|
// structuredContent is the data object that gets passed to ontoolresult
|
|
// The UI accesses it via result.structuredContent
|
|
return {
|
|
content: [{ type: 'text', text: textSummary }],
|
|
structuredContent: data
|
|
};
|
|
}
|
|
/**
|
|
* Inject data into HTML as a script tag
|
|
*/
|
|
injectDataIntoHTML(html, data) {
|
|
const dataScript = `<script>window.__MCP_APP_DATA__ = ${JSON.stringify(data)};</script>`;
|
|
// Insert before </head> or at the beginning of <body>
|
|
if (html.includes('</head>')) {
|
|
return html.replace('</head>', `${dataScript}</head>`);
|
|
}
|
|
else if (html.includes('<body>')) {
|
|
return html.replace('<body>', `<body>${dataScript}`);
|
|
}
|
|
else {
|
|
return dataScript + html;
|
|
}
|
|
}
|
|
/**
|
|
* Get resource handler by URI
|
|
*/
|
|
getResourceHandler(uri) {
|
|
return this.resourceHandlers.get(uri);
|
|
}
|
|
/**
|
|
* Get all registered resource URIs
|
|
*/
|
|
getResourceURIs() {
|
|
return Array.from(this.resourceHandlers.keys());
|
|
}
|
|
}
|