From 19b03fe777b18657c9e31ec79e740fac568cfa03 Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Thu, 29 Jan 2026 17:46:52 -0500 Subject: [PATCH] Add all 11 MCP Apps with proper _meta.ui.resourceUri - Fixed UI build path to dist/app-ui/ - Added _meta.ui.resourceUri to all 11 tool definitions - Changed MIME type to text/html;profile=mcp-app - Added resources capability and handlers (ListResourcesRequestSchema, ReadResourceRequestSchema) - Apps: contact-grid, pipeline-board, quick-book, opportunity-card, calendar-view, invoice-preview, campaign-stats, agent-stats, contact-timeline, workflow-status, dashboard Co-Authored-By: Claude Opus 4.5 --- src/apps/index.ts | 750 ++++++++++++++++++++++++++++++++++++++++++++++ src/server.ts | 442 ++++++++++++++++++++++++++- 2 files changed, 1187 insertions(+), 5 deletions(-) create mode 100644 src/apps/index.ts diff --git a/src/apps/index.ts b/src/apps/index.ts new file mode 100644 index 0000000..0b8162c --- /dev/null +++ b/src/apps/index.ts @@ -0,0 +1,750 @@ +/** + * MCP Apps Manager + * Manages rich UI components for GoHighLevel MCP Server + */ + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { GHLApiClient } from '../clients/ghl-api-client.js'; +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +export interface AppToolResult { + content: Array<{ type: 'text'; text: string }>; + structuredContent?: { + type: 'resource'; + resource: { + uri: string; + mimeType: string; + text?: string; + blob?: string; + }; + }; + [key: string]: unknown; +} + +export interface AppResourceHandler { + uri: string; + mimeType: string; + getContent: () => string; +} + +/** + * MCP Apps Manager class + * Registers app tools and handles structuredContent responses + */ +// Note: We use process.cwd() based path resolution to find UI dist +// This works when running from the project root directory +function getUIBuildPath(): string { + // First try dist/app-ui (where MCP Apps are built) + const appUiPath = path.join(process.cwd(), 'dist', 'app-ui'); + if (fs.existsSync(appUiPath)) { + return appUiPath; + } + // Fallback to src/ui/dist for legacy UI components + const fromCwd = path.join(process.cwd(), 'src', 'ui', 'dist'); + if (fs.existsSync(fromCwd)) { + return fromCwd; + } + // Default fallback + return appUiPath; +} + +export class MCPAppsManager { + private ghlClient: GHLApiClient; + private resourceHandlers: Map = new Map(); + private uiBuildPath: string; + + constructor(ghlClient: GHLApiClient) { + this.ghlClient = ghlClient; + this.uiBuildPath = getUIBuildPath(); + process.stderr.write(`[MCP Apps] UI build path: ${this.uiBuildPath}\n`); + this.registerResourceHandlers(); + } + + /** + * Register all UI resource handlers + */ + private registerResourceHandlers(): void { + const resources: Array<{ uri: string; file: string }> = [ + // 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 + */ + private loadUIResource(filename: string): string { + 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 + */ + private getFallbackHTML(filename: string): string { + const componentName = filename.replace('.html', ''); + return ` + + + + + + GHL ${componentName} + + + +
+

UI component "${componentName}" is loading...

+

Run npm run build:ui to build UI components.

+
+ + + + `.trim(); + } + + /** + * Get tool definitions for all app tools + */ + getToolDefinitions(): Tool[] { + 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' } + } + } + ]; + } + + /** + * Get app tool names for routing + */ + getAppToolNames(): string[] { + 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' + ]; + } + + /** + * Check if a tool is an app tool + */ + isAppTool(toolName: string): boolean { + return this.getAppToolNames().includes(toolName); + } + + /** + * Execute an app tool + */ + async executeTool(toolName: string, args: Record): Promise { + 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(); + default: + throw new Error(`Unknown app tool: ${toolName}`); + } + } + + /** + * View contact grid (search results) + */ + private async viewContactGrid(query?: string, limit?: number): Promise { + 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) + */ + private async viewPipelineBoard(pipelineId: string): Promise { + 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: any) => p.id === pipelineId); + const opportunities = opportunitiesResponse.data?.opportunities || []; + + const data = { + pipeline, + opportunities, + 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 + */ + private async viewQuickBook(calendarId: string, contactId?: string): Promise { + 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 as any)?.name || calendarId}`, + resourceHandler.uri, + resourceHandler.mimeType, + resourceHandler.getContent(), + data + ); + } + + /** + * View opportunity card + */ + private async viewOpportunityCard(opportunityId: string): Promise { + 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 as any)?.name || opportunityId}`, + resourceHandler.uri, + resourceHandler.mimeType, + resourceHandler.getContent(), + opportunity + ); + } + + /** + * View calendar + */ + private async viewCalendar(calendarId: string, startDate?: string, endDate?: string): Promise { + 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 as any; + 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 + */ + private async viewCampaignStats(campaignId: string): Promise { + // Get email campaigns + const response = await this.ghlClient.getEmailCampaigns({}); + + const campaigns = response.data?.schedules || []; + const campaign = campaigns.find((c: any) => 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 as any)?.name || campaignId}`, + resourceHandler.uri, + resourceHandler.mimeType, + resourceHandler.getContent(), + data + ); + } + + /** + * View agent stats + */ + private async viewAgentStats(userId?: string, dateRange?: string): Promise { + // 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 + */ + private async viewContactTimeline(contactId: string): Promise { + 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 as any; + 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 + */ + private async viewWorkflowStatus(workflowId: string): Promise { + const response = await this.ghlClient.getWorkflows({ + locationId: this.ghlClient.getConfig().locationId + }); + + const workflows = response.data?.workflows || []; + const workflow = workflows.find((w: any) => 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 as any)?.name || workflowId}`, + resourceHandler.uri, + resourceHandler.mimeType, + resourceHandler.getContent(), + data + ); + } + + /** + * View main dashboard + */ + private async viewDashboard(): Promise { + 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 + */ + private async viewInvoice(invoiceId: string): Promise { + 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 + ); + } + + /** + * Create app tool result with structuredContent + */ + private createAppResult( + textSummary: string, + resourceUri: string, + mimeType: string, + htmlContent: string, + data: any + ): AppToolResult { + // Inject the data into the HTML + const htmlWithData = this.injectDataIntoHTML(htmlContent, data); + + return { + content: [{ type: 'text', text: textSummary }], + structuredContent: { + type: 'resource', + resource: { + uri: resourceUri, + mimeType: mimeType, + text: htmlWithData + } + } + }; + } + + /** + * Inject data into HTML as a script tag + */ + private injectDataIntoHTML(html: string, data: any): string { + const dataScript = ``; + + // Insert before or at the beginning of + if (html.includes('')) { + return html.replace('', `${dataScript}`); + } else if (html.includes('')) { + return html.replace('', `${dataScript}`); + } else { + return dataScript + html; + } + } + + /** + * Get resource handler by URI + */ + getResourceHandler(uri: string): AppResourceHandler | undefined { + return this.resourceHandlers.get(uri); + } + + /** + * Get all registered resource URIs + */ + getResourceURIs(): string[] { + return Array.from(this.resourceHandlers.keys()); + } +} diff --git a/src/server.ts b/src/server.ts index c930090..a877199 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,11 +5,13 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { +import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, - McpError + ListResourcesRequestSchema, + ReadResourceRequestSchema, + McpError } from '@modelcontextprotocol/sdk/types.js'; import * as dotenv from 'dotenv'; @@ -34,6 +36,28 @@ import { GHLConfig } from './types/ghl-types'; import { ProductsTools } from './tools/products-tools.js'; import { PaymentsTools } from './tools/payments-tools.js'; import { InvoicesTools } from './tools/invoices-tools.js'; +// New tools +import { FormsTools } from './tools/forms-tools.js'; +import { UsersTools } from './tools/users-tools.js'; +import { FunnelsTools } from './tools/funnels-tools.js'; +import { BusinessesTools } from './tools/businesses-tools.js'; +import { LinksTools } from './tools/links-tools.js'; +import { CompaniesTools } from './tools/companies-tools.js'; +import { SaasTools } from './tools/saas-tools.js'; +import { SnapshotsTools } from './tools/snapshots-tools.js'; +// Additional comprehensive tools +import { CoursesTools } from './tools/courses-tools.js'; +import { CampaignsTools } from './tools/campaigns-tools.js'; +import { ReportingTools } from './tools/reporting-tools.js'; +import { OAuthTools } from './tools/oauth-tools.js'; +import { WebhooksTools } from './tools/webhooks-tools.js'; +import { PhoneTools } from './tools/phone-tools.js'; +import { ReputationTools } from './tools/reputation-tools.js'; +import { AffiliatesTools } from './tools/affiliates-tools.js'; +import { TemplatesTools } from './tools/templates-tools.js'; +import { SmartListsTools } from './tools/smartlists-tools.js'; +import { TriggersTools } from './tools/triggers-tools.js'; +import { MCPAppsManager } from './apps/index.js'; // Load environment variables dotenv.config(); @@ -63,6 +87,28 @@ class GHLMCPServer { private productsTools: ProductsTools; private paymentsTools: PaymentsTools; private invoicesTools: InvoicesTools; + // New tools + private formsTools: FormsTools; + private usersTools: UsersTools; + private funnelsTools: FunnelsTools; + private businessesTools: BusinessesTools; + private linksTools: LinksTools; + private companiesTools: CompaniesTools; + private saasTools: SaasTools; + private snapshotsTools: SnapshotsTools; + // Additional comprehensive tools + private coursesTools: CoursesTools; + private campaignsTools: CampaignsTools; + private reportingTools: ReportingTools; + private oauthTools: OAuthTools; + private webhooksTools: WebhooksTools; + private phoneTools: PhoneTools; + private reputationTools: ReputationTools; + private affiliatesTools: AffiliatesTools; + private templatesTools: TemplatesTools; + private smartListsTools: SmartListsTools; + private triggersTools: TriggersTools; + private mcpAppsManager: MCPAppsManager; constructor() { // Initialize MCP server with capabilities @@ -74,6 +120,7 @@ class GHLMCPServer { { capabilities: { tools: {}, + resources: {}, }, } ); @@ -101,6 +148,30 @@ class GHLMCPServer { this.productsTools = new ProductsTools(this.ghlClient); this.paymentsTools = new PaymentsTools(this.ghlClient); this.invoicesTools = new InvoicesTools(this.ghlClient); + // New tools + this.formsTools = new FormsTools(this.ghlClient); + this.usersTools = new UsersTools(this.ghlClient); + this.funnelsTools = new FunnelsTools(this.ghlClient); + this.businessesTools = new BusinessesTools(this.ghlClient); + this.linksTools = new LinksTools(this.ghlClient); + this.companiesTools = new CompaniesTools(this.ghlClient); + this.saasTools = new SaasTools(this.ghlClient); + this.snapshotsTools = new SnapshotsTools(this.ghlClient); + // Additional comprehensive tools + this.coursesTools = new CoursesTools(this.ghlClient); + this.campaignsTools = new CampaignsTools(this.ghlClient); + this.reportingTools = new ReportingTools(this.ghlClient); + this.oauthTools = new OAuthTools(this.ghlClient); + this.webhooksTools = new WebhooksTools(this.ghlClient); + this.phoneTools = new PhoneTools(this.ghlClient); + this.reputationTools = new ReputationTools(this.ghlClient); + this.affiliatesTools = new AffiliatesTools(this.ghlClient); + this.templatesTools = new TemplatesTools(this.ghlClient); + this.smartListsTools = new SmartListsTools(this.ghlClient); + this.triggersTools = new TriggersTools(this.ghlClient); + + // Initialize MCP Apps Manager for rich UI components + this.mcpAppsManager = new MCPAppsManager(this.ghlClient); // Setup MCP handlers this.setupHandlers(); @@ -139,6 +210,41 @@ class GHLMCPServer { * Setup MCP request handlers */ private setupHandlers(): void { + // Handle list resources requests (for MCP Apps) + this.server.setRequestHandler(ListResourcesRequestSchema, async () => { + process.stderr.write('[GHL MCP] Listing resources...\n'); + const resourceUris = this.mcpAppsManager.getResourceURIs(); + return { + resources: resourceUris.map(uri => { + const handler = this.mcpAppsManager.getResourceHandler(uri); + return { + uri, + name: uri.replace('ui://ghl/', ''), + mimeType: handler?.mimeType || 'text/html;profile=mcp-app' + }; + }) + }; + }); + + // Handle read resource requests (for MCP Apps) + this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const { uri } = request.params; + process.stderr.write(`[GHL MCP] Reading resource: ${uri}\n`); + + const handler = this.mcpAppsManager.getResourceHandler(uri); + if (!handler) { + throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${uri}`); + } + + return { + contents: [{ + uri, + mimeType: handler.mimeType, + text: handler.getContent() + }] + }; + }); + // Handle list tools requests this.server.setRequestHandler(ListToolsRequestSchema, async () => { process.stderr.write('[GHL MCP] Listing available tools...\n'); @@ -163,7 +269,29 @@ class GHLMCPServer { const productsToolDefinitions = this.productsTools.getTools(); const paymentsToolDefinitions = this.paymentsTools.getTools(); const invoicesToolDefinitions = this.invoicesTools.getTools(); - + // New tools + const formsToolDefinitions = this.formsTools.getToolDefinitions(); + const usersToolDefinitions = this.usersTools.getToolDefinitions(); + const funnelsToolDefinitions = this.funnelsTools.getToolDefinitions(); + const businessesToolDefinitions = this.businessesTools.getToolDefinitions(); + const linksToolDefinitions = this.linksTools.getToolDefinitions(); + const companiesToolDefinitions = this.companiesTools.getToolDefinitions(); + const saasToolDefinitions = this.saasTools.getToolDefinitions(); + const snapshotsToolDefinitions = this.snapshotsTools.getToolDefinitions(); + // Additional comprehensive tools + const coursesToolDefinitions = this.coursesTools.getToolDefinitions(); + const campaignsToolDefinitions = this.campaignsTools.getToolDefinitions(); + const reportingToolDefinitions = this.reportingTools.getToolDefinitions(); + const oauthToolDefinitions = this.oauthTools.getToolDefinitions(); + const webhooksToolDefinitions = this.webhooksTools.getToolDefinitions(); + const phoneToolDefinitions = this.phoneTools.getToolDefinitions(); + const reputationToolDefinitions = this.reputationTools.getToolDefinitions(); + const affiliatesToolDefinitions = this.affiliatesTools.getToolDefinitions(); + const templatesToolDefinitions = this.templatesTools.getToolDefinitions(); + const smartListsToolDefinitions = this.smartListsTools.getToolDefinitions(); + const triggersToolDefinitions = this.triggersTools.getToolDefinitions(); + const appToolDefinitions = this.mcpAppsManager.getToolDefinitions(); + const allTools = [ ...contactToolDefinitions, ...conversationToolDefinitions, @@ -183,7 +311,30 @@ class GHLMCPServer { ...storeToolDefinitions, ...productsToolDefinitions, ...paymentsToolDefinitions, - ...invoicesToolDefinitions + ...invoicesToolDefinitions, + // New tools + ...formsToolDefinitions, + ...usersToolDefinitions, + ...funnelsToolDefinitions, + ...businessesToolDefinitions, + ...linksToolDefinitions, + ...companiesToolDefinitions, + ...saasToolDefinitions, + ...snapshotsToolDefinitions, + // Additional comprehensive tools + ...coursesToolDefinitions, + ...campaignsToolDefinitions, + ...reportingToolDefinitions, + ...oauthToolDefinitions, + ...webhooksToolDefinitions, + ...phoneToolDefinitions, + ...reputationToolDefinitions, + ...affiliatesToolDefinitions, + ...templatesToolDefinitions, + ...smartListsToolDefinitions, + ...triggersToolDefinitions, + // MCP Apps (rich UI components) + ...appToolDefinitions ]; process.stderr.write(`[GHL MCP] Registered ${allTools.length} tools total:\n`); @@ -206,7 +357,29 @@ class GHLMCPServer { process.stderr.write(`[GHL MCP] - ${productsToolDefinitions.length} products tools\n`); process.stderr.write(`[GHL MCP] - ${paymentsToolDefinitions.length} payments tools\n`); process.stderr.write(`[GHL MCP] - ${invoicesToolDefinitions.length} invoices tools\n`); - + // New tools logging + process.stderr.write(`[GHL MCP] - ${formsToolDefinitions.length} forms tools\n`); + process.stderr.write(`[GHL MCP] - ${usersToolDefinitions.length} users tools\n`); + process.stderr.write(`[GHL MCP] - ${funnelsToolDefinitions.length} funnels tools\n`); + process.stderr.write(`[GHL MCP] - ${businessesToolDefinitions.length} businesses tools\n`); + process.stderr.write(`[GHL MCP] - ${linksToolDefinitions.length} links tools\n`); + process.stderr.write(`[GHL MCP] - ${companiesToolDefinitions.length} companies tools\n`); + process.stderr.write(`[GHL MCP] - ${saasToolDefinitions.length} saas tools\n`); + process.stderr.write(`[GHL MCP] - ${snapshotsToolDefinitions.length} snapshots tools\n`); + // Additional comprehensive tools logging + process.stderr.write(`[GHL MCP] - ${coursesToolDefinitions.length} courses tools\n`); + process.stderr.write(`[GHL MCP] - ${campaignsToolDefinitions.length} campaigns tools\n`); + process.stderr.write(`[GHL MCP] - ${reportingToolDefinitions.length} reporting tools\n`); + process.stderr.write(`[GHL MCP] - ${oauthToolDefinitions.length} oauth tools\n`); + process.stderr.write(`[GHL MCP] - ${webhooksToolDefinitions.length} webhooks tools\n`); + process.stderr.write(`[GHL MCP] - ${phoneToolDefinitions.length} phone tools\n`); + process.stderr.write(`[GHL MCP] - ${reputationToolDefinitions.length} reputation tools\n`); + process.stderr.write(`[GHL MCP] - ${affiliatesToolDefinitions.length} affiliates tools\n`); + process.stderr.write(`[GHL MCP] - ${templatesToolDefinitions.length} templates tools\n`); + process.stderr.write(`[GHL MCP] - ${smartListsToolDefinitions.length} smart lists tools\n`); + process.stderr.write(`[GHL MCP] - ${triggersToolDefinitions.length} triggers tools\n`); + process.stderr.write(`[GHL MCP] - ${appToolDefinitions.length} MCP App tools (rich UI)\n`); + return { tools: allTools }; @@ -229,6 +402,13 @@ class GHLMCPServer { try { let result: any; + // Check if this is an MCP App tool (returns structuredContent) + if (this.mcpAppsManager.isAppTool(name)) { + const appResult = await this.mcpAppsManager.executeTool(name, args || {}); + process.stderr.write(`[GHL MCP] App tool ${name} executed successfully\n`); + return appResult; + } + // Route to appropriate tool handler if (this.isContactTool(name)) { result = await this.contactTools.executeTool(name, args || {}); @@ -268,6 +448,46 @@ class GHLMCPServer { result = await this.paymentsTools.handleToolCall(name, args || {}); } else if (this.isInvoicesTool(name)) { result = await this.invoicesTools.handleToolCall(name, args || {}); + // New tools + } else if (this.isFormsTool(name)) { + result = await this.formsTools.handleToolCall(name, args || {}); + } else if (this.isUsersTool(name)) { + result = await this.usersTools.handleToolCall(name, args || {}); + } else if (this.isFunnelsTool(name)) { + result = await this.funnelsTools.handleToolCall(name, args || {}); + } else if (this.isBusinessesTool(name)) { + result = await this.businessesTools.handleToolCall(name, args || {}); + } else if (this.isLinksTool(name)) { + result = await this.linksTools.handleToolCall(name, args || {}); + } else if (this.isCompaniesTool(name)) { + result = await this.companiesTools.handleToolCall(name, args || {}); + } else if (this.isSaasTool(name)) { + result = await this.saasTools.handleToolCall(name, args || {}); + } else if (this.isSnapshotsTool(name)) { + result = await this.snapshotsTools.handleToolCall(name, args || {}); + // Additional comprehensive tools + } else if (this.isCoursesTool(name)) { + result = await this.coursesTools.handleToolCall(name, args || {}); + } else if (this.isCampaignsTool(name)) { + result = await this.campaignsTools.handleToolCall(name, args || {}); + } else if (this.isReportingTool(name)) { + result = await this.reportingTools.handleToolCall(name, args || {}); + } else if (this.isOAuthTool(name)) { + result = await this.oauthTools.handleToolCall(name, args || {}); + } else if (this.isWebhooksTool(name)) { + result = await this.webhooksTools.handleToolCall(name, args || {}); + } else if (this.isPhoneTool(name)) { + result = await this.phoneTools.handleToolCall(name, args || {}); + } else if (this.isReputationTool(name)) { + result = await this.reputationTools.handleToolCall(name, args || {}); + } else if (this.isAffiliatesTool(name)) { + result = await this.affiliatesTools.handleToolCall(name, args || {}); + } else if (this.isTemplatesTool(name)) { + result = await this.templatesTools.handleToolCall(name, args || {}); + } else if (this.isSmartListsTool(name)) { + result = await this.smartListsTools.handleToolCall(name, args || {}); + } else if (this.isTriggersTool(name)) { + result = await this.triggersTools.handleToolCall(name, args || {}); } else { throw new Error(`Unknown tool: ${name}`); } @@ -598,7 +818,219 @@ class GHLMCPServer { return invoicesToolNames.includes(toolName); } + /** + * Check if tool name belongs to forms tools + */ + private isFormsTool(toolName: string): boolean { + const formsToolNames = ['get_forms', 'get_form_submissions', 'get_form_by_id']; + return formsToolNames.includes(toolName); + } + /** + * Check if tool name belongs to users tools + */ + private isUsersTool(toolName: string): boolean { + const usersToolNames = ['get_users', 'get_user', 'create_user', 'update_user', 'delete_user', 'search_users']; + return usersToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to funnels tools + */ + private isFunnelsTool(toolName: string): boolean { + const funnelsToolNames = [ + 'get_funnels', 'get_funnel', 'get_funnel_pages', 'count_funnel_pages', + 'create_funnel_redirect', 'update_funnel_redirect', 'delete_funnel_redirect', 'get_funnel_redirects' + ]; + return funnelsToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to businesses tools + */ + private isBusinessesTool(toolName: string): boolean { + const businessesToolNames = ['get_businesses', 'get_business', 'create_business', 'update_business', 'delete_business']; + return businessesToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to links tools + */ + private isLinksTool(toolName: string): boolean { + const linksToolNames = ['get_links', 'get_link', 'create_link', 'update_link', 'delete_link']; + return linksToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to companies tools + */ + private isCompaniesTool(toolName: string): boolean { + const companiesToolNames = ['get_companies', 'get_company', 'create_company', 'update_company', 'delete_company']; + return companiesToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to saas tools + */ + private isSaasTool(toolName: string): boolean { + const saasToolNames = [ + 'get_saas_locations', 'get_saas_location', 'update_saas_subscription', + 'pause_saas_location', 'enable_saas_location', 'rebilling_update' + ]; + return saasToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to snapshots tools + */ + private isSnapshotsTool(toolName: string): boolean { + const snapshotsToolNames = [ + 'get_snapshots', 'get_snapshot', 'create_snapshot', + 'get_snapshot_push_status', 'get_latest_snapshot_push', 'push_snapshot_to_subaccounts' + ]; + return snapshotsToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to courses tools + */ + private isCoursesTool(toolName: string): boolean { + const coursesToolNames = [ + 'get_courses', 'get_course', 'create_course', 'update_course', 'delete_course', + 'publish_course', 'unpublish_course', 'get_course_products', 'get_course_offers', + 'create_course_offer', 'update_course_offer', 'delete_course_offer', + 'get_course_instructors', 'add_course_instructor', 'remove_course_instructor', + 'get_course_categories', 'create_course_category', 'update_course_category', 'delete_course_category', + 'get_course_lessons', 'get_course_lesson', 'create_course_lesson', 'update_course_lesson', 'delete_course_lesson', + 'reorder_lessons', 'get_course_students', 'enroll_student', 'unenroll_student', + 'get_student_progress', 'update_student_progress', 'reset_student_progress', + 'complete_lesson', 'uncomplete_lesson' + ]; + return coursesToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to campaigns tools + */ + private isCampaignsTool(toolName: string): boolean { + const campaignsToolNames = [ + 'get_campaigns', 'get_campaign', 'create_campaign', 'update_campaign', 'delete_campaign', + 'get_campaign_stats', 'get_campaign_contacts', 'add_campaign_contacts', 'remove_campaign_contacts', + 'pause_campaign', 'resume_campaign' + ]; + return campaignsToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to reporting tools + */ + private isReportingTool(toolName: string): boolean { + const reportingToolNames = [ + 'get_dashboard_stats', 'get_conversion_report', 'get_attribution_report', + 'get_call_report', 'get_appointment_report', 'get_email_report', 'get_sms_report', + 'get_pipeline_report', 'get_revenue_report', 'get_ad_report' + ]; + return reportingToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to oauth tools + */ + private isOAuthTool(toolName: string): boolean { + const oauthToolNames = [ + 'get_installed_locations', 'get_location_access_token', 'generate_location_token', + 'refresh_access_token', 'get_oauth_config', 'get_token_info' + ]; + return oauthToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to webhooks tools + */ + private isWebhooksTool(toolName: string): boolean { + const webhooksToolNames = [ + 'get_webhooks', 'get_webhook', 'create_webhook', 'update_webhook', 'delete_webhook', + 'get_webhook_events', 'test_webhook' + ]; + return webhooksToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to phone tools + */ + private isPhoneTool(toolName: string): boolean { + const phoneToolNames = [ + 'get_phone_numbers', 'get_phone_number', 'search_available_numbers', 'purchase_phone_number', + 'update_phone_number', 'release_phone_number', 'get_call_forwarding_settings', 'update_call_forwarding', + 'get_ivr_menus', 'create_ivr_menu', 'update_ivr_menu', 'delete_ivr_menu', + 'get_voicemail_settings', 'update_voicemail_settings', 'get_voicemails', 'delete_voicemail', + 'get_caller_ids', 'add_caller_id', 'verify_caller_id', 'delete_caller_id' + ]; + return phoneToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to reputation tools + */ + private isReputationTool(toolName: string): boolean { + const reputationToolNames = [ + 'get_reviews', 'get_review', 'reply_to_review', 'update_review_reply', 'delete_review_reply', + 'get_review_stats', 'send_review_request', 'get_review_requests', + 'get_connected_review_platforms', 'connect_google_business', 'disconnect_review_platform', + 'get_review_links', 'update_review_links', 'get_review_widget_settings', 'update_review_widget_settings' + ]; + return reputationToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to affiliates tools + */ + private isAffiliatesTool(toolName: string): boolean { + const affiliatesToolNames = [ + 'get_affiliate_campaigns', 'get_affiliate_campaign', 'create_affiliate_campaign', + 'update_affiliate_campaign', 'delete_affiliate_campaign', + 'get_affiliates', 'get_affiliate', 'create_affiliate', 'update_affiliate', + 'approve_affiliate', 'reject_affiliate', 'delete_affiliate', + 'get_affiliate_commissions', 'get_affiliate_stats', 'create_payout', 'get_payouts', 'get_referrals' + ]; + return affiliatesToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to templates tools + */ + private isTemplatesTool(toolName: string): boolean { + const templatesToolNames = [ + 'get_sms_templates', 'get_sms_template', 'create_sms_template', 'update_sms_template', 'delete_sms_template', + 'get_voicemail_templates', 'create_voicemail_template', 'delete_voicemail_template', + 'get_social_templates', 'create_social_template', 'delete_social_template', + 'get_whatsapp_templates', 'create_whatsapp_template', 'delete_whatsapp_template', + 'get_snippets', 'create_snippet', 'update_snippet', 'delete_snippet' + ]; + return templatesToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to smart lists tools + */ + private isSmartListsTool(toolName: string): boolean { + const smartListsToolNames = [ + 'get_smart_lists', 'get_smart_list', 'create_smart_list', 'update_smart_list', 'delete_smart_list', + 'get_smart_list_contacts', 'get_smart_list_count', 'duplicate_smart_list' + ]; + return smartListsToolNames.includes(toolName); + } + + /** + * Check if tool name belongs to triggers tools + */ + private isTriggersTool(toolName: string): boolean { + const triggersToolNames = [ + 'get_triggers', 'get_trigger', 'create_trigger', 'update_trigger', 'delete_trigger', + 'enable_trigger', 'disable_trigger', 'get_trigger_types', 'get_trigger_logs', 'test_trigger', 'duplicate_trigger' + ]; + return triggersToolNames.includes(toolName); + } /** * Test GHL API connection