Daily backup: 2026-01-30
This commit is contained in:
parent
cbc2f5e973
commit
30d55b5899
@ -15,4 +15,4 @@
|
|||||||
Building bulk animation generator for MCP marketing. Using Remotion with canvas viewport technique (dolly camera). Camera should zoom into typing area, follow text as typed, zoom out when done. Category-specific questions per software type.
|
Building bulk animation generator for MCP marketing. Using Remotion with canvas viewport technique (dolly camera). Camera should zoom into typing area, follow text as typed, zoom out when done. Category-specific questions per software type.
|
||||||
|
|
||||||
---
|
---
|
||||||
*Last updated: 2026-01-28 23:00 EST*
|
*Last updated: 2026-01-30 23:00 EST*
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"command": "node",
|
"command": "node",
|
||||||
"args": ["/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/GoHighLevel-MCP/dist/server.js"],
|
"args": ["/Users/jakeshore/.clawdbot/workspace/mcp-diagrams/GoHighLevel-MCP/dist/server.js"],
|
||||||
"env": {
|
"env": {
|
||||||
"GHL_API_KEY": "pit-0aebc49f-07f7-47dc-a494-181b72a1df54",
|
"GHL_API_KEY": "pit-0480982f-750b-4baa-bc10-1340a9b2102b",
|
||||||
"GHL_BASE_URL": "https://services.leadconnectorhq.com",
|
"GHL_BASE_URL": "https://services.leadconnectorhq.com",
|
||||||
"GHL_LOCATION_ID": "DZEpRd43MxUJKdtrev9t",
|
"GHL_LOCATION_ID": "DZEpRd43MxUJKdtrev9t",
|
||||||
"NODE_ENV": "production"
|
"NODE_ENV": "production"
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
Subproject commit 69db02d7cf9aca4fc39ae34b6f20f26617e57316
|
Subproject commit 19b03fe777b18657c9e31ec79e740fac568cfa03
|
||||||
11
mcp-diagrams/ghl-mcp-apps-only/.env.example
Normal file
11
mcp-diagrams/ghl-mcp-apps-only/.env.example
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# GoHighLevel API Configuration
|
||||||
|
GHL_API_KEY=your_private_integration_api_key_here
|
||||||
|
GHL_BASE_URL=https://services.leadconnectorhq.com
|
||||||
|
GHL_LOCATION_ID=your_location_id_here
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
MCP_SERVER_PORT=8000
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Optional: For AI features
|
||||||
|
OPENAI_API_KEY=your_openai_key_here_optional
|
||||||
481
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/agent-stats.html
vendored
Normal file
481
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/agent-stats.html
vendored
Normal file
File diff suppressed because one or more lines are too long
544
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/calendar-view.html
vendored
Normal file
544
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/calendar-view.html
vendored
Normal file
File diff suppressed because one or more lines are too long
442
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/campaign-stats.html
vendored
Normal file
442
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/campaign-stats.html
vendored
Normal file
File diff suppressed because one or more lines are too long
501
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/contact-grid.html
vendored
Normal file
501
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/contact-grid.html
vendored
Normal file
File diff suppressed because one or more lines are too long
355
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/contact-timeline.html
vendored
Normal file
355
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/contact-timeline.html
vendored
Normal file
File diff suppressed because one or more lines are too long
474
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/invoice-preview.html
vendored
Normal file
474
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/invoice-preview.html
vendored
Normal file
File diff suppressed because one or more lines are too long
168
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/mcp-app.html
vendored
Normal file
168
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/mcp-app.html
vendored
Normal file
File diff suppressed because one or more lines are too long
520
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/opportunity-card.html
vendored
Normal file
520
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/opportunity-card.html
vendored
Normal file
File diff suppressed because one or more lines are too long
533
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/pipeline-board.html
vendored
Normal file
533
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/pipeline-board.html
vendored
Normal file
File diff suppressed because one or more lines are too long
453
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/quick-book.html
vendored
Normal file
453
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/quick-book.html
vendored
Normal file
File diff suppressed because one or more lines are too long
352
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/workflow-status.html
vendored
Normal file
352
mcp-diagrams/ghl-mcp-apps-only/dist/app-ui/workflow-status.html
vendored
Normal file
File diff suppressed because one or more lines are too long
654
mcp-diagrams/ghl-mcp-apps-only/dist/apps/index.js
vendored
Normal file
654
mcp-diagrams/ghl-mcp-apps-only/dist/apps/index.js
vendored
Normal file
@ -0,0 +1,654 @@
|
|||||||
|
/**
|
||||||
|
* 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
5107
mcp-diagrams/ghl-mcp-apps-only/dist/clients/ghl-api-client.js
vendored
Normal file
5107
mcp-diagrams/ghl-mcp-apps-only/dist/clients/ghl-api-client.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
139
mcp-diagrams/ghl-mcp-apps-only/dist/server.js
vendored
Normal file
139
mcp-diagrams/ghl-mcp-apps-only/dist/server.js
vendored
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* GoHighLevel MCP Apps Server (Slimmed Down)
|
||||||
|
* Only includes MCP Apps - no other tools
|
||||||
|
*/
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import { GHLApiClient } from './clients/ghl-api-client.js';
|
||||||
|
import { MCPAppsManager } from './apps/index.js';
|
||||||
|
// Load environment variables
|
||||||
|
dotenv.config();
|
||||||
|
/**
|
||||||
|
* MCP Apps Only Server
|
||||||
|
*/
|
||||||
|
class GHLMCPAppsServer {
|
||||||
|
server;
|
||||||
|
ghlClient;
|
||||||
|
mcpAppsManager;
|
||||||
|
constructor() {
|
||||||
|
// Initialize MCP server with capabilities
|
||||||
|
this.server = new Server({
|
||||||
|
name: 'ghl-mcp-apps-only',
|
||||||
|
version: '1.0.0',
|
||||||
|
}, {
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Initialize GHL API client
|
||||||
|
this.ghlClient = this.initializeGHLClient();
|
||||||
|
// Initialize MCP Apps Manager
|
||||||
|
this.mcpAppsManager = new MCPAppsManager(this.ghlClient);
|
||||||
|
this.setupHandlers();
|
||||||
|
this.setupErrorHandling();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Initialize the GHL API client with config from environment
|
||||||
|
*/
|
||||||
|
initializeGHLClient() {
|
||||||
|
const config = {
|
||||||
|
accessToken: process.env.GHL_API_KEY || '',
|
||||||
|
locationId: process.env.GHL_LOCATION_ID || '',
|
||||||
|
baseUrl: process.env.GHL_BASE_URL || 'https://services.leadconnectorhq.com',
|
||||||
|
version: '2021-07-28'
|
||||||
|
};
|
||||||
|
if (!config.accessToken) {
|
||||||
|
process.stderr.write('Warning: GHL_API_KEY not set in environment\n');
|
||||||
|
}
|
||||||
|
if (!config.locationId) {
|
||||||
|
process.stderr.write('Warning: GHL_LOCATION_ID not set in environment\n');
|
||||||
|
}
|
||||||
|
return new GHLApiClient(config);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Setup request handlers
|
||||||
|
*/
|
||||||
|
setupHandlers() {
|
||||||
|
// List tools - only MCP App tools
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
const appTools = this.mcpAppsManager.getToolDefinitions();
|
||||||
|
process.stderr.write(`[MCP Apps Only] Listing ${appTools.length} app tools\n`);
|
||||||
|
return { tools: appTools };
|
||||||
|
});
|
||||||
|
// List resources - MCP App UI resources
|
||||||
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
const resourceUris = this.mcpAppsManager.getResourceURIs();
|
||||||
|
const resources = resourceUris.map(uri => {
|
||||||
|
const handler = this.mcpAppsManager.getResourceHandler(uri);
|
||||||
|
return {
|
||||||
|
uri: uri,
|
||||||
|
name: uri,
|
||||||
|
mimeType: handler?.mimeType || 'text/html;profile=mcp-app'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
process.stderr.write(`[MCP Apps Only] Listing ${resources.length} UI resources\n`);
|
||||||
|
return { resources };
|
||||||
|
});
|
||||||
|
// Read resource - serve UI HTML
|
||||||
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const uri = request.params.uri;
|
||||||
|
process.stderr.write(`[MCP Apps Only] Reading resource: ${uri}\n`);
|
||||||
|
const handler = this.mcpAppsManager.getResourceHandler(uri);
|
||||||
|
if (!handler) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${uri}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
contents: [{
|
||||||
|
uri: uri,
|
||||||
|
mimeType: handler.mimeType,
|
||||||
|
text: handler.getContent()
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Call tool - execute MCP App tools
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
process.stderr.write(`[MCP Apps Only] Calling tool: ${name}\n`);
|
||||||
|
if (!this.mcpAppsManager.isAppTool(name)) {
|
||||||
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await this.mcpAppsManager.executeTool(name, args || {});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
process.stderr.write(`[MCP Apps Only] Tool error: ${error.message}\n`);
|
||||||
|
throw new McpError(ErrorCode.InternalError, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Setup error handling
|
||||||
|
*/
|
||||||
|
setupErrorHandling() {
|
||||||
|
this.server.onerror = (error) => {
|
||||||
|
process.stderr.write(`[MCP Apps Only] Server error: ${error}\n`);
|
||||||
|
};
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
await this.server.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Start the server
|
||||||
|
*/
|
||||||
|
async run() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await this.server.connect(transport);
|
||||||
|
process.stderr.write('[MCP Apps Only] Server started - Apps only mode\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Start server
|
||||||
|
const server = new GHLMCPAppsServer();
|
||||||
|
server.run().catch((error) => {
|
||||||
|
process.stderr.write(`Failed to start server: ${error}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
5
mcp-diagrams/ghl-mcp-apps-only/dist/types/ghl-types.js
vendored
Normal file
5
mcp-diagrams/ghl-mcp-apps-only/dist/types/ghl-types.js
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* TypeScript interfaces for GoHighLevel API integration
|
||||||
|
* Based on official OpenAPI specifications v2021-07-28 (Contacts) and v2021-04-15 (Conversations)
|
||||||
|
*/
|
||||||
|
export {};
|
||||||
22
mcp-diagrams/ghl-mcp-apps-only/package.json
Normal file
22
mcp-diagrams/ghl-mcp-apps-only/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "ghl-mcp-apps-only",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "GoHighLevel MCP Apps Only - Slimmed down for testing",
|
||||||
|
"main": "dist/server.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/server.js",
|
||||||
|
"dev": "tsx src/server.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"dotenv": "^16.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.15.29",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
824
mcp-diagrams/ghl-mcp-apps-only/src/apps/index.ts
Normal file
824
mcp-diagrams/ghl-mcp-apps-only/src/apps/index.ts
Normal file
@ -0,0 +1,824 @@
|
|||||||
|
/**
|
||||||
|
* 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?: Record<string, unknown>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppResourceHandler {
|
||||||
|
uri: string;
|
||||||
|
mimeType: string;
|
||||||
|
getContent: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(): string {
|
||||||
|
// 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 {
|
||||||
|
private ghlClient: GHLApiClient;
|
||||||
|
private resourceHandlers: Map<string, AppResourceHandler> = 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 `
|
||||||
|
<!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(): 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' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 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(): 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',
|
||||||
|
'update_opportunity'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string, any>): Promise<AppToolResult> {
|
||||||
|
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 as {
|
||||||
|
opportunityId: string;
|
||||||
|
pipelineStageId?: string;
|
||||||
|
name?: string;
|
||||||
|
monetaryValue?: number;
|
||||||
|
status?: 'open' | 'won' | 'lost' | 'abandoned';
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown app tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View contact grid (search results)
|
||||||
|
*/
|
||||||
|
private async viewContactGrid(query?: string, limit?: number): Promise<AppToolResult> {
|
||||||
|
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<AppToolResult> {
|
||||||
|
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 || [];
|
||||||
|
|
||||||
|
// Simplify opportunity data to only include fields the UI needs (reduces payload size)
|
||||||
|
const simplifiedOpportunities = opportunities.map((opp: any) => ({
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private async viewQuickBook(calendarId: string, contactId?: string): Promise<AppToolResult> {
|
||||||
|
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<AppToolResult> {
|
||||||
|
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<AppToolResult> {
|
||||||
|
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<AppToolResult> {
|
||||||
|
// 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<AppToolResult> {
|
||||||
|
// 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<AppToolResult> {
|
||||||
|
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<AppToolResult> {
|
||||||
|
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<AppToolResult> {
|
||||||
|
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<AppToolResult> {
|
||||||
|
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)
|
||||||
|
*/
|
||||||
|
private async updateOpportunity(args: {
|
||||||
|
opportunityId: string;
|
||||||
|
pipelineStageId?: string;
|
||||||
|
name?: string;
|
||||||
|
monetaryValue?: number;
|
||||||
|
status?: 'open' | 'won' | 'lost' | 'abandoned';
|
||||||
|
}): Promise<AppToolResult> {
|
||||||
|
const { opportunityId, ...updates } = args;
|
||||||
|
|
||||||
|
// Build the update payload
|
||||||
|
const updatePayload: any = {};
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
private createAppResult(
|
||||||
|
textSummary: string,
|
||||||
|
resourceUri: string,
|
||||||
|
mimeType: string,
|
||||||
|
htmlContent: string,
|
||||||
|
data: any
|
||||||
|
): AppToolResult {
|
||||||
|
// 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
|
||||||
|
*/
|
||||||
|
private injectDataIntoHTML(html: string, data: any): string {
|
||||||
|
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: string): AppResourceHandler | undefined {
|
||||||
|
return this.resourceHandlers.get(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered resource URIs
|
||||||
|
*/
|
||||||
|
getResourceURIs(): string[] {
|
||||||
|
return Array.from(this.resourceHandlers.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
6858
mcp-diagrams/ghl-mcp-apps-only/src/clients/ghl-api-client.ts
Normal file
6858
mcp-diagrams/ghl-mcp-apps-only/src/clients/ghl-api-client.ts
Normal file
File diff suppressed because it is too large
Load Diff
172
mcp-diagrams/ghl-mcp-apps-only/src/server.ts
Normal file
172
mcp-diagrams/ghl-mcp-apps-only/src/server.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* GoHighLevel MCP Apps Server (Slimmed Down)
|
||||||
|
* Only includes MCP Apps - no other tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ErrorCode,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
McpError
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
import { GHLApiClient } from './clients/ghl-api-client.js';
|
||||||
|
import { MCPAppsManager } from './apps/index.js';
|
||||||
|
import { GHLConfig } from './types/ghl-types.js';
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Apps Only Server
|
||||||
|
*/
|
||||||
|
class GHLMCPAppsServer {
|
||||||
|
private server: Server;
|
||||||
|
private ghlClient: GHLApiClient;
|
||||||
|
private mcpAppsManager: MCPAppsManager;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Initialize MCP server with capabilities
|
||||||
|
this.server = new Server(
|
||||||
|
{
|
||||||
|
name: 'ghl-mcp-apps-only',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize GHL API client
|
||||||
|
this.ghlClient = this.initializeGHLClient();
|
||||||
|
|
||||||
|
// Initialize MCP Apps Manager
|
||||||
|
this.mcpAppsManager = new MCPAppsManager(this.ghlClient);
|
||||||
|
|
||||||
|
this.setupHandlers();
|
||||||
|
this.setupErrorHandling();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the GHL API client with config from environment
|
||||||
|
*/
|
||||||
|
private initializeGHLClient(): GHLApiClient {
|
||||||
|
const config: GHLConfig = {
|
||||||
|
accessToken: process.env.GHL_API_KEY || '',
|
||||||
|
locationId: process.env.GHL_LOCATION_ID || '',
|
||||||
|
baseUrl: process.env.GHL_BASE_URL || 'https://services.leadconnectorhq.com',
|
||||||
|
version: '2021-07-28'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config.accessToken) {
|
||||||
|
process.stderr.write('Warning: GHL_API_KEY not set in environment\n');
|
||||||
|
}
|
||||||
|
if (!config.locationId) {
|
||||||
|
process.stderr.write('Warning: GHL_LOCATION_ID not set in environment\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GHLApiClient(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup request handlers
|
||||||
|
*/
|
||||||
|
private setupHandlers(): void {
|
||||||
|
// List tools - only MCP App tools
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
const appTools = this.mcpAppsManager.getToolDefinitions();
|
||||||
|
process.stderr.write(`[MCP Apps Only] Listing ${appTools.length} app tools\n`);
|
||||||
|
return { tools: appTools };
|
||||||
|
});
|
||||||
|
|
||||||
|
// List resources - MCP App UI resources
|
||||||
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
const resourceUris = this.mcpAppsManager.getResourceURIs();
|
||||||
|
const resources = resourceUris.map(uri => {
|
||||||
|
const handler = this.mcpAppsManager.getResourceHandler(uri);
|
||||||
|
return {
|
||||||
|
uri: uri,
|
||||||
|
name: uri,
|
||||||
|
mimeType: handler?.mimeType || 'text/html;profile=mcp-app'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
process.stderr.write(`[MCP Apps Only] Listing ${resources.length} UI resources\n`);
|
||||||
|
return { resources };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read resource - serve UI HTML
|
||||||
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const uri = request.params.uri;
|
||||||
|
process.stderr.write(`[MCP Apps Only] Reading resource: ${uri}\n`);
|
||||||
|
|
||||||
|
const handler = this.mcpAppsManager.getResourceHandler(uri);
|
||||||
|
if (!handler) {
|
||||||
|
throw new McpError(ErrorCode.InvalidRequest, `Resource not found: ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [{
|
||||||
|
uri: uri,
|
||||||
|
mimeType: handler.mimeType,
|
||||||
|
text: handler.getContent()
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call tool - execute MCP App tools
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
process.stderr.write(`[MCP Apps Only] Calling tool: ${name}\n`);
|
||||||
|
|
||||||
|
if (!this.mcpAppsManager.isAppTool(name)) {
|
||||||
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.mcpAppsManager.executeTool(name, args || {});
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
process.stderr.write(`[MCP Apps Only] Tool error: ${error.message}\n`);
|
||||||
|
throw new McpError(ErrorCode.InternalError, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup error handling
|
||||||
|
*/
|
||||||
|
private setupErrorHandling(): void {
|
||||||
|
this.server.onerror = (error) => {
|
||||||
|
process.stderr.write(`[MCP Apps Only] Server error: ${error}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
await this.server.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server
|
||||||
|
*/
|
||||||
|
async run(): Promise<void> {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await this.server.connect(transport);
|
||||||
|
process.stderr.write('[MCP Apps Only] Server started - Apps only mode\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
const server = new GHLMCPAppsServer();
|
||||||
|
server.run().catch((error) => {
|
||||||
|
process.stderr.write(`Failed to start server: ${error}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
6688
mcp-diagrams/ghl-mcp-apps-only/src/types/ghl-types.ts
Normal file
6688
mcp-diagrams/ghl-mcp-apps-only/src/types/ghl-types.ts
Normal file
File diff suppressed because it is too large
Load Diff
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/calendar-widget-CUbShwNj.js
vendored
Normal file
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/calendar-widget-CUbShwNj.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/contact-card-CFJe96SR.js
vendored
Normal file
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/contact-card-CFJe96SR.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
import{j as e,A as h,g as n,f as d,R as m,a as g}from"./styles-CphAgR3l.js";function o({contact:s}){const a=s.name||`${s.firstName||""} ${s.lastName||""}`.trim()||"Unknown Contact",i=n(s.firstName,s.lastName),r=()=>{const l=[s.address1,s.city,s.state,s.postalCode,s.country].filter(Boolean);return l.length>0?l.join(", "):null};return e.jsx("div",{className:"ghl-app",children:e.jsxs("div",{className:"ghl-card",children:[e.jsx("div",{className:"ghl-card-header",children:e.jsxs("div",{className:"ghl-flex ghl-items-center ghl-gap-4",children:[e.jsx("div",{className:"ghl-avatar ghl-avatar-lg",style:{background:x(a)},children:i}),e.jsxs("div",{children:[e.jsx("h2",{style:{fontSize:20,fontWeight:600,marginBottom:4},children:a}),s.companyName&&e.jsx("p",{className:"ghl-text-secondary",children:s.companyName})]})]})}),e.jsxs("div",{className:"ghl-card-body",children:[e.jsxs("div",{className:"ghl-grid ghl-grid-2",style:{gap:16},children:[s.email&&e.jsxs("div",{children:[e.jsx("label",{className:"ghl-text-sm ghl-text-muted",style:{display:"block",marginBottom:4},children:"Email"}),e.jsx("a",{href:`mailto:${s.email}`,style:{color:"var(--ghl-primary)",textDecoration:"none"},children:s.email})]}),s.phone&&e.jsxs("div",{children:[e.jsx("label",{className:"ghl-text-sm ghl-text-muted",style:{display:"block",marginBottom:4},children:"Phone"}),e.jsx("a",{href:`tel:${s.phone}`,style:{color:"var(--ghl-primary)",textDecoration:"none"},children:s.phone})]}),s.website&&e.jsxs("div",{children:[e.jsx("label",{className:"ghl-text-sm ghl-text-muted",style:{display:"block",marginBottom:4},children:"Website"}),e.jsx("a",{href:s.website,target:"_blank",rel:"noopener noreferrer",style:{color:"var(--ghl-primary)",textDecoration:"none"},children:s.website})]}),s.source&&e.jsxs("div",{children:[e.jsx("label",{className:"ghl-text-sm ghl-text-muted",style:{display:"block",marginBottom:4},children:"Source"}),e.jsx("span",{children:s.source})]}),r()&&e.jsxs("div",{style:{gridColumn:"span 2"},children:[e.jsx("label",{className:"ghl-text-sm ghl-text-muted",style:{display:"block",marginBottom:4},children:"Address"}),e.jsx("span",{children:r()})]})]}),s.tags&&s.tags.length>0&&e.jsxs("div",{style:{marginTop:16},children:[e.jsx("label",{className:"ghl-text-sm ghl-text-muted",style:{display:"block",marginBottom:8},children:"Tags"}),e.jsx("div",{className:"ghl-flex ghl-gap-2",style:{flexWrap:"wrap"},children:s.tags.map((l,t)=>e.jsx("span",{className:"ghl-badge ghl-badge-primary",children:l},t))})]}),s.customFields&&s.customFields.length>0&&e.jsxs("div",{style:{marginTop:16},children:[e.jsx("label",{className:"ghl-text-sm ghl-text-muted",style:{display:"block",marginBottom:8},children:"Custom Fields"}),e.jsx("div",{className:"ghl-grid ghl-grid-2",style:{gap:12},children:s.customFields.slice(0,6).map(l=>e.jsxs("div",{children:[e.jsxs("span",{className:"ghl-text-sm ghl-text-muted",children:[l.key,": "]}),e.jsx("span",{children:String(l.value)})]},l.id))})]})]}),e.jsx("div",{className:"ghl-card-footer",children:e.jsxs("div",{className:"ghl-flex ghl-justify-between ghl-text-sm ghl-text-muted",children:[e.jsxs("span",{children:["Added: ",d(s.dateAdded)]}),e.jsxs("span",{children:["Updated: ",d(s.dateUpdated)]})]})})]})})}function x(s){let a=0;for(let r=0;r<s.length;r++)a=s.charCodeAt(r)+((a<<5)-a);const i=["#4f46e5","#7c3aed","#2563eb","#0891b2","#059669","#d97706","#dc2626","#db2777","#9333ea","#0d9488"];return i[Math.abs(a)%i.length]}function c(){return e.jsx(h,{children:s=>e.jsx(o,{contact:s})})}m.createRoot(document.getElementById("root")).render(g.createElement(c));
|
||||||
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/contact-grid-C_Uxn-WJ.js
vendored
Normal file
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/contact-grid-C_Uxn-WJ.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
import{j as e,A as k,r as g,g as w,f as A,R as $,a as z}from"./styles-CphAgR3l.js";function D({data:a}){const[l,o]=g.useState("name"),[n,y]=g.useState("asc"),[c,b]=g.useState(1),[d,x]=g.useState(new Set),j=10,p=a.contacts||[],v=[...p].sort((s,t)=>{let i,r;switch(l){case"name":i=s.name||`${s.firstName||""} ${s.lastName||""}`.trim(),r=t.name||`${t.firstName||""} ${t.lastName||""}`.trim();break;case"email":i=s.email||"",r=t.email||"";break;case"dateAdded":i=s.dateAdded||"",r=t.dateAdded||"";break;default:return 0}const m=i.localeCompare(r);return n==="asc"?m:-m}),u=Math.ceil(v.length/j),h=v.slice((c-1)*j,c*j),N=s=>{l===s?y(n==="asc"?"desc":"asc"):(o(s),y("asc"))},C=s=>{const t=new Set(d);t.has(s)?t.delete(s):t.add(s),x(t)},S=()=>{d.size===h.length?x(new Set):x(new Set(h.map(s=>s.id)))},f=({field:s})=>e.jsx("span",{style:{marginLeft:4,opacity:l===s?1:.3},children:l===s&&n==="desc"?"↓":"↑"});return p.length===0?e.jsx("div",{className:"ghl-app",children:e.jsxs("div",{className:"ghl-empty",children:[e.jsx("div",{className:"ghl-empty-icon",children:"👥"}),e.jsx("p",{children:"No contacts found"})]})}):e.jsx("div",{className:"ghl-app",children:e.jsxs("div",{className:"ghl-card",children:[e.jsx("div",{className:"ghl-card-header",children:e.jsxs("div",{className:"ghl-flex ghl-justify-between ghl-items-center",children:[e.jsxs("div",{children:[e.jsx("h2",{style:{fontSize:18,fontWeight:600},children:"Contacts"}),e.jsxs("p",{className:"ghl-text-sm ghl-text-muted",children:[a.total||p.length," total contacts"]})]}),d.size>0&&e.jsx("div",{className:"ghl-flex ghl-gap-2",children:e.jsxs("span",{className:"ghl-text-sm ghl-text-secondary",children:[d.size," selected"]})})]})}),e.jsx("div",{style:{overflowX:"auto"},children:e.jsxs("table",{className:"ghl-table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{style:{width:40},children:e.jsx("input",{type:"checkbox",checked:d.size===h.length&&h.length>0,onChange:S})}),e.jsxs("th",{onClick:()=>N("name"),style:{cursor:"pointer"},children:["Contact ",e.jsx(f,{field:"name"})]}),e.jsxs("th",{onClick:()=>N("email"),style:{cursor:"pointer"},children:["Email ",e.jsx(f,{field:"email"})]}),e.jsx("th",{children:"Phone"}),e.jsx("th",{children:"Tags"}),e.jsxs("th",{onClick:()=>N("dateAdded"),style:{cursor:"pointer"},children:["Added ",e.jsx(f,{field:"dateAdded"})]})]})}),e.jsx("tbody",{children:h.map(s=>{const t=s.name||`${s.firstName||""} ${s.lastName||""}`.trim()||"Unknown",i=w(s.firstName,s.lastName);return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("input",{type:"checkbox",checked:d.has(s.id),onChange:()=>C(s.id)})}),e.jsx("td",{children:e.jsxs("div",{className:"ghl-flex ghl-items-center ghl-gap-2",children:[e.jsx("div",{className:"ghl-avatar ghl-avatar-sm",style:{background:P(t)},children:i}),e.jsxs("div",{children:[e.jsx("div",{className:"ghl-font-medium",children:t}),s.companyName&&e.jsx("div",{className:"ghl-text-sm ghl-text-muted",children:s.companyName})]})]})}),e.jsx("td",{children:s.email?e.jsx("a",{href:`mailto:${s.email}`,style:{color:"var(--ghl-primary)",textDecoration:"none"},children:s.email}):e.jsx("span",{className:"ghl-text-muted",children:"-"})}),e.jsx("td",{children:s.phone?e.jsx("a",{href:`tel:${s.phone}`,style:{color:"var(--ghl-primary)",textDecoration:"none"},children:s.phone}):e.jsx("span",{className:"ghl-text-muted",children:"-"})}),e.jsx("td",{children:e.jsxs("div",{className:"ghl-flex ghl-gap-2",style:{flexWrap:"wrap",maxWidth:200},children:[(s.tags||[]).slice(0,3).map((r,m)=>e.jsx("span",{className:"ghl-badge ghl-badge-primary",children:r},m)),(s.tags||[]).length>3&&e.jsxs("span",{className:"ghl-badge",children:["+",s.tags.length-3]})]})}),e.jsx("td",{className:"ghl-text-muted",children:A(s.dateAdded)})]},s.id)})})]})}),u>1&&e.jsx("div",{className:"ghl-card-footer",children:e.jsxs("div",{className:"ghl-flex ghl-justify-between ghl-items-center",children:[e.jsxs("span",{className:"ghl-text-sm ghl-text-muted",children:["Page ",c," of ",u]}),e.jsxs("div",{className:"ghl-flex ghl-gap-2",children:[e.jsx("button",{className:"ghl-btn ghl-btn-secondary ghl-btn-sm",disabled:c===1,onClick:()=>b(s=>s-1),children:"Previous"}),e.jsx("button",{className:"ghl-btn ghl-btn-secondary ghl-btn-sm",disabled:c===u,onClick:()=>b(s=>s+1),children:"Next"})]})]})})]})})}function P(a){let l=0;for(let n=0;n<a.length;n++)l=a.charCodeAt(n)+((l<<5)-l);const o=["#4f46e5","#7c3aed","#2563eb","#0891b2","#059669","#d97706","#dc2626","#db2777","#9333ea","#0d9488"];return o[Math.abs(l)%o.length]}function E(){return e.jsx(k,{children:a=>e.jsx(D,{data:a})})}$.createRoot(document.getElementById("root")).render(z.createElement(E));
|
||||||
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/conversation-thread-DBGTC45D.js
vendored
Normal file
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/conversation-thread-DBGTC45D.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
import{j as e,A as x,r as h,R as g,a as u}from"./styles-CphAgR3l.js";function m({data:t}){const{conversation:n,messages:c}=t,l=h.useRef(null),d=n.contactName||n.contact?.name||`${n.contact?.firstName||""} ${n.contact?.lastName||""}`.trim()||"Unknown Contact";h.useEffect(()=>{l.current?.scrollIntoView({behavior:"smooth"})},[]);const r=[...c||[]].sort((a,s)=>{const o=a.dateAdded?new Date(a.dateAdded).getTime():0,p=s.dateAdded?new Date(s.dateAdded).getTime():0;return o-p}),i=new Map;for(const a of r){const s=a.dateAdded?new Date(a.dateAdded).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}):"Unknown Date";i.has(s)||i.set(s,[]),i.get(s).push(a)}return e.jsx("div",{className:"ghl-app",style:{height:"100%",display:"flex",flexDirection:"column"},children:e.jsxs("div",{className:"ghl-card",style:{flex:1,display:"flex",flexDirection:"column",maxHeight:600},children:[e.jsx("div",{className:"ghl-card-header",children:e.jsxs("div",{className:"ghl-flex ghl-items-center ghl-gap-4",children:[e.jsx("div",{style:{width:44,height:44,borderRadius:"50%",background:"var(--ghl-primary)",display:"flex",alignItems:"center",justifyContent:"center",color:"white",fontWeight:600},children:d.charAt(0).toUpperCase()}),e.jsxs("div",{children:[e.jsx("h2",{style:{fontSize:16,fontWeight:600,marginBottom:2},children:d}),e.jsxs("div",{className:"ghl-flex ghl-items-center ghl-gap-2",children:[n.contact?.phone&&e.jsx("span",{className:"ghl-text-sm ghl-text-muted",children:n.contact.phone}),n.type&&e.jsx("span",{className:"ghl-badge",children:n.type})]})]})]})}),e.jsx("div",{style:{flex:1,overflowY:"auto",padding:16,background:"var(--ghl-bg-secondary)"},children:r.length===0?e.jsxs("div",{className:"ghl-empty",children:[e.jsx("div",{className:"ghl-empty-icon",children:"💬"}),e.jsx("p",{children:"No messages in this conversation"})]}):e.jsxs(e.Fragment,{children:[Array.from(i.entries()).map(([a,s])=>e.jsxs("div",{children:[e.jsxs("div",{style:{textAlign:"center",margin:"16px 0",position:"relative"},children:[e.jsx("span",{style:{background:"var(--ghl-bg-secondary)",padding:"4px 12px",fontSize:12,color:"var(--ghl-text-muted)",position:"relative",zIndex:1},children:a}),e.jsx("div",{style:{position:"absolute",top:"50%",left:0,right:0,height:1,background:"var(--ghl-border)",zIndex:0}})]}),s.map(o=>e.jsx(f,{message:o},o.id))]},a)),e.jsx("div",{ref:l})]})}),e.jsx("div",{className:"ghl-card-footer",children:e.jsxs("div",{className:"ghl-flex ghl-items-center ghl-gap-2",children:[e.jsxs("div",{style:{flex:1,padding:"10px 14px",background:"var(--ghl-bg)",border:"1px solid var(--ghl-border)",borderRadius:20,fontSize:14,color:"var(--ghl-text-muted)"},children:["Reply to ",d,"..."]}),e.jsx("button",{className:"ghl-btn ghl-btn-primary",style:{borderRadius:20,padding:"10px 20px"},children:"Send"})]})})]})})}function f({message:t}){const n=t.direction==="outbound",c=r=>r?new Date(r).toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"}):"",l=r=>{switch(r){case"delivered":return"✓✓";case"sent":return"✓";case"read":return"✓✓";case"failed":return"✕";default:return""}},d=r=>{switch(r){case"SMS":return"📱";case"Email":return"📧";case"WhatsApp":return"💬";case"FB":return"👤";case"GMB":return"🏢";case"Call":return"📞";default:return""}};return e.jsx("div",{style:{display:"flex",justifyContent:n?"flex-end":"flex-start",marginBottom:8},children:e.jsxs("div",{style:{maxWidth:"75%",background:n?"var(--ghl-primary)":"var(--ghl-bg)",color:n?"white":"var(--ghl-text)",padding:"10px 14px",borderRadius:n?"18px 18px 4px 18px":"18px 18px 18px 4px",boxShadow:"var(--ghl-shadow)"},children:[t.type&&e.jsxs("div",{style:{fontSize:10,marginBottom:4,opacity:.7},children:[d(t.type)," ",t.type]}),e.jsx("div",{style:{wordBreak:"break-word",whiteSpace:"pre-wrap"},children:t.body||e.jsx("span",{style:{opacity:.6,fontStyle:"italic"},children:"[No content]"})}),t.attachments&&t.attachments.length>0&&e.jsx("div",{style:{marginTop:8},children:t.attachments.map((r,i)=>e.jsx("div",{style:{padding:"6px 10px",background:n?"rgba(255,255,255,0.1)":"var(--ghl-bg-secondary)",borderRadius:8,fontSize:12,marginTop:4},children:"📎 Attachment"},i))}),e.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"flex-end",gap:6,marginTop:6,fontSize:11,opacity:.7},children:[e.jsx("span",{children:c(t.dateAdded)}),n&&t.status&&e.jsx("span",{style:{color:t.status==="failed"?"#ef4444":"inherit"},children:l(t.status)})]})]})})}function y(){return e.jsx(x,{children:t=>e.jsx(m,{data:t})})}g.createRoot(document.getElementById("root")).render(u.createElement(y));
|
||||||
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/invoice-preview-DphRvjHB.js
vendored
Normal file
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/invoice-preview-DphRvjHB.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/opportunity-kanban-CSzRcmdW.js
vendored
Normal file
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/opportunity-kanban-CSzRcmdW.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
import{j as e,A as u,b as g,f as p,R as v,a as j}from"./styles-CphAgR3l.js";function f({data:s}){const{pipeline:n,opportunities:l,stages:h}=s,c=[...h||[]].sort((t,a)=>(t.position||0)-(a.position||0)),r=new Map;for(const t of c)r.set(t.id,[]);for(const t of l||[]){const a=r.get(t.pipelineStageId||"");a&&a.push(t)}const m=t=>{const a=r.get(t)||[],i=a.length,d=a.reduce((o,x)=>o+(x.monetaryValue||0),0);return{count:i,value:d}};return n?e.jsxs("div",{className:"ghl-app",children:[e.jsxs("div",{style:{marginBottom:16},children:[e.jsx("h2",{style:{fontSize:20,fontWeight:600,marginBottom:4},children:n.name}),e.jsxs("p",{className:"ghl-text-sm ghl-text-muted",children:[l?.length||0," opportunities | ",c.length," stages"]})]}),e.jsx("div",{style:{display:"flex",gap:16,overflowX:"auto",paddingBottom:16},children:c.map(t=>{const{count:a,value:i}=m(t.id),d=r.get(t.id)||[];return e.jsxs("div",{style:{flex:"0 0 280px",display:"flex",flexDirection:"column",maxHeight:500},children:[e.jsxs("div",{style:{padding:"12px 16px",background:"var(--ghl-bg-secondary)",borderRadius:"var(--ghl-radius) var(--ghl-radius) 0 0",border:"1px solid var(--ghl-border)",borderBottom:"none"},children:[e.jsxs("div",{className:"ghl-flex ghl-justify-between ghl-items-center",children:[e.jsx("span",{className:"ghl-font-semibold",children:t.name}),e.jsx("span",{className:"ghl-badge",children:a})]}),i>0&&e.jsx("div",{className:"ghl-text-sm ghl-text-muted",style:{marginTop:4},children:g(i)})]}),e.jsx("div",{style:{flex:1,padding:8,background:"var(--ghl-bg-tertiary)",borderRadius:"0 0 var(--ghl-radius) var(--ghl-radius)",border:"1px solid var(--ghl-border)",borderTop:"none",overflowY:"auto",minHeight:200},children:d.length===0?e.jsx("div",{style:{padding:20,textAlign:"center",color:"var(--ghl-text-muted)",fontSize:13},children:"No opportunities"}):e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:8},children:d.map(o=>e.jsx(b,{opportunity:o},o.id))})})]},t.id)})}),e.jsx("div",{className:"ghl-card",style:{marginTop:16},children:e.jsx("div",{className:"ghl-card-body",children:e.jsxs("div",{className:"ghl-flex ghl-justify-between",children:[e.jsxs("div",{children:[e.jsx("span",{className:"ghl-text-muted",children:"Total Opportunities"}),e.jsx("div",{className:"ghl-font-semibold",style:{fontSize:20},children:l?.length||0})]}),e.jsxs("div",{style:{textAlign:"right"},children:[e.jsx("span",{className:"ghl-text-muted",children:"Total Pipeline Value"}),e.jsx("div",{className:"ghl-font-semibold",style:{fontSize:20,color:"var(--ghl-success)"},children:g(l?.reduce((t,a)=>t+(a.monetaryValue||0),0)||0)})]})]})})})]}):e.jsx("div",{className:"ghl-app",children:e.jsxs("div",{className:"ghl-empty",children:[e.jsx("div",{className:"ghl-empty-icon",children:"📊"}),e.jsx("p",{children:"Pipeline not found"})]})})}function b({opportunity:s}){const n=s.contact?.name||`${s.contact?.firstName||""} ${s.contact?.lastName||""}`.trim()||"Unknown Contact",l={open:"var(--ghl-primary)",won:"var(--ghl-success)",lost:"var(--ghl-danger)",abandoned:"var(--ghl-warning)"};return e.jsxs("div",{style:{background:"var(--ghl-bg)",borderRadius:"var(--ghl-radius)",padding:12,boxShadow:"var(--ghl-shadow)",border:"1px solid var(--ghl-border)"},children:[e.jsx("div",{className:"ghl-font-medium",style:{marginBottom:8},children:s.name}),e.jsxs("div",{className:"ghl-flex ghl-items-center ghl-gap-2",style:{marginBottom:8},children:[e.jsx("span",{style:{width:24,height:24,borderRadius:"50%",background:"var(--ghl-bg-tertiary)",display:"flex",alignItems:"center",justifyContent:"center",fontSize:10},children:"👤"}),e.jsx("span",{className:"ghl-text-sm",children:n})]}),e.jsxs("div",{className:"ghl-flex ghl-justify-between ghl-items-center",children:[s.monetaryValue?e.jsx("span",{className:"ghl-font-semibold",style:{color:"var(--ghl-success)"},children:g(s.monetaryValue)}):e.jsx("span",{className:"ghl-text-muted ghl-text-sm",children:"No value"}),s.status&&e.jsx("span",{className:"ghl-badge",style:{background:`${l[s.status]||"var(--ghl-bg-tertiary)"}20`,color:l[s.status]||"var(--ghl-text-muted)"},children:s.status})]}),s.dateAdded&&e.jsxs("div",{className:"ghl-text-sm ghl-text-muted",style:{marginTop:8},children:["Added ",p(s.dateAdded)]})]})}function y(){return e.jsx(u,{children:s=>e.jsx(f,{data:s})})}v.createRoot(document.getElementById("root")).render(j.createElement(y));
|
||||||
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/style-BaFxk78P.css
vendored
Normal file
1
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/style-BaFxk78P.css
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
:root{--ghl-primary: #4f46e5;--ghl-primary-hover: #4338ca;--ghl-success: #22c55e;--ghl-warning: #f59e0b;--ghl-danger: #ef4444;--ghl-info: #3b82f6;--ghl-bg: #ffffff;--ghl-bg-secondary: #f9fafb;--ghl-bg-tertiary: #f3f4f6;--ghl-text: #111827;--ghl-text-secondary: #6b7280;--ghl-text-muted: #9ca3af;--ghl-border: #e5e7eb;--ghl-border-dark: #d1d5db;--ghl-shadow: 0 1px 3px rgba(0, 0, 0, .1);--ghl-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .1);--ghl-radius: 8px;--ghl-radius-lg: 12px}*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:14px;line-height:1.5;color:var(--ghl-text);background:var(--ghl-bg)}.ghl-app{padding:16px;max-width:100%;overflow-x:hidden}.ghl-card{background:var(--ghl-bg);border:1px solid var(--ghl-border);border-radius:var(--ghl-radius-lg);box-shadow:var(--ghl-shadow);overflow:hidden}.ghl-card-header{padding:16px;border-bottom:1px solid var(--ghl-border);background:var(--ghl-bg-secondary)}.ghl-card-body{padding:16px}.ghl-card-footer{padding:12px 16px;border-top:1px solid var(--ghl-border);background:var(--ghl-bg-secondary)}.ghl-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 16px;font-size:14px;font-weight:500;border-radius:var(--ghl-radius);border:1px solid transparent;cursor:pointer;transition:all .15s ease}.ghl-btn-primary{background:var(--ghl-primary);color:#fff}.ghl-btn-primary:hover{background:var(--ghl-primary-hover)}.ghl-btn-secondary{background:var(--ghl-bg);color:var(--ghl-text);border-color:var(--ghl-border)}.ghl-btn-secondary:hover{background:var(--ghl-bg-secondary)}.ghl-btn-sm{padding:4px 10px;font-size:12px}.ghl-badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:12px;font-weight:500;border-radius:9999px;background:var(--ghl-bg-tertiary);color:var(--ghl-text-secondary)}.ghl-badge-primary{background:#4f46e51a;color:var(--ghl-primary)}.ghl-badge-success{background:#22c55e1a;color:var(--ghl-success)}.ghl-badge-warning{background:#f59e0b1a;color:var(--ghl-warning)}.ghl-badge-danger{background:#ef44441a;color:var(--ghl-danger)}.ghl-avatar{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;background:var(--ghl-primary);color:#fff;font-weight:600;font-size:16px}.ghl-avatar-lg{width:64px;height:64px;font-size:24px}.ghl-avatar-sm{width:32px;height:32px;font-size:12px}.ghl-table{width:100%;border-collapse:collapse}.ghl-table th,.ghl-table td{padding:12px;text-align:left;border-bottom:1px solid var(--ghl-border)}.ghl-table th{font-weight:600;background:var(--ghl-bg-secondary);color:var(--ghl-text-secondary);font-size:12px;text-transform:uppercase;letter-spacing:.05em}.ghl-table tr:hover{background:var(--ghl-bg-secondary)}.ghl-grid{display:grid;gap:16px}.ghl-grid-2{grid-template-columns:repeat(2,1fr)}.ghl-grid-3{grid-template-columns:repeat(3,1fr)}.ghl-grid-4{grid-template-columns:repeat(4,1fr)}.ghl-flex{display:flex}.ghl-flex-col{flex-direction:column}.ghl-items-center{align-items:center}.ghl-justify-between{justify-content:space-between}.ghl-gap-2{gap:8px}.ghl-gap-4{gap:16px}.ghl-text-sm{font-size:12px}.ghl-text-lg{font-size:18px}.ghl-text-muted{color:var(--ghl-text-muted)}.ghl-text-secondary{color:var(--ghl-text-secondary)}.ghl-font-medium{font-weight:500}.ghl-font-semibold{font-weight:600}.ghl-status-paid{color:var(--ghl-success)}.ghl-status-pending{color:var(--ghl-warning)}.ghl-status-overdue{color:var(--ghl-danger)}.ghl-status-draft{color:var(--ghl-text-muted)}.ghl-loading{display:flex;align-items:center;justify-content:center;padding:40px;color:var(--ghl-text-muted)}.ghl-spinner{width:24px;height:24px;border:2px solid var(--ghl-border);border-top-color:var(--ghl-primary);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.ghl-empty{text-align:center;padding:40px;color:var(--ghl-text-muted)}.ghl-empty-icon{font-size:48px;margin-bottom:16px;opacity:.5}
|
||||||
9
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/styles-CphAgR3l.js
vendored
Normal file
9
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/assets/styles-CphAgR3l.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/calendar-widget.html
vendored
Normal file
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/calendar-widget.html
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendar Widget - GHL MCP</title>
|
||||||
|
<script type="module" crossorigin src="/assets/calendar-widget-CUbShwNj.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/styles-CphAgR3l.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/style-BaFxk78P.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/contact-card.html
vendored
Normal file
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/contact-card.html
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Contact Card - GHL MCP</title>
|
||||||
|
<script type="module" crossorigin src="/assets/contact-card-CFJe96SR.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/styles-CphAgR3l.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/style-BaFxk78P.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/contact-grid.html
vendored
Normal file
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/contact-grid.html
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Contact Grid - GHL MCP</title>
|
||||||
|
<script type="module" crossorigin src="/assets/contact-grid-C_Uxn-WJ.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/styles-CphAgR3l.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/style-BaFxk78P.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/conversation-thread.html
vendored
Normal file
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/conversation-thread.html
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Conversation Thread - GHL MCP</title>
|
||||||
|
<script type="module" crossorigin src="/assets/conversation-thread-DBGTC45D.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/styles-CphAgR3l.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/style-BaFxk78P.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/invoice-preview.html
vendored
Normal file
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/invoice-preview.html
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Invoice Preview - GHL MCP</title>
|
||||||
|
<script type="module" crossorigin src="/assets/invoice-preview-DphRvjHB.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/styles-CphAgR3l.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/style-BaFxk78P.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/opportunity-kanban.html
vendored
Normal file
14
mcp-diagrams/ghl-mcp-apps-only/src/ui/dist/opportunity-kanban.html
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Pipeline Kanban - GHL MCP</title>
|
||||||
|
<script type="module" crossorigin src="/assets/opportunity-kanban-CSzRcmdW.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="/assets/styles-CphAgR3l.js">
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/style-BaFxk78P.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
mcp-diagrams/ghl-mcp-apps-only/tsconfig.json
Normal file
16
mcp-diagrams/ghl-mcp-apps-only/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests/**/*"]
|
||||||
|
}
|
||||||
33
memory/2026-01-30.md
Normal file
33
memory/2026-01-30.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Memory Log - 2026-01-30
|
||||||
|
|
||||||
|
## Work Completed
|
||||||
|
|
||||||
|
### MCP Animation Framework (Remotion)
|
||||||
|
- **Status:** Dolly camera version completed using canvas viewport technique
|
||||||
|
- **Technical approach:** Camera zooms into typing area, follows text as typed, zooms out when done
|
||||||
|
- **Features:** Category-specific questions per software type for MCP marketing bulk generation
|
||||||
|
- **Current state:** Waiting for Jake's feedback on camera movement to iterate
|
||||||
|
|
||||||
|
### Recently Active Projects
|
||||||
|
- **fortura-assets:** 60 native components in component library
|
||||||
|
- **memory system:** Ongoing improvements to daily memory/logging system
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
- Using canvas viewport technique for dolly camera effect in Remotion (cleaner than direct camera transforms)
|
||||||
|
- Category-specific questions strategy for different software types in MCP marketing videos
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
- Awaiting feedback on MCP Animation Framework dolly camera movement
|
||||||
|
- Based on feedback, may need to iterate on camera smoothness, timing, or framing
|
||||||
|
- Once approved, move forward with bulk animation generation pipeline
|
||||||
|
|
||||||
|
## Notable Context for Future Me
|
||||||
|
- MCP Animation Framework is a bulk animation generator for MCP marketing
|
||||||
|
- The core challenge is making typing animations feel dynamic and engaging
|
||||||
|
- Dolly camera approach adds visual interest without being distracting
|
||||||
|
- Different software categories need tailored questions to make videos relevant
|
||||||
|
|
||||||
|
## System Updates
|
||||||
|
- Daily memory checkpoint system is working (cron job triggered successfully)
|
||||||
|
- HEARTBEAT.md being updated with current task state
|
||||||
|
- Git backup routine in place
|
||||||
@ -1,111 +1,53 @@
|
|||||||
# Burton Method Research Intel
|
# Burton Method Competitor Research Intel
|
||||||
|
|
||||||
> **How this works:** Current week's in-depth intel lives at the top. Each week, I compress the previous week into 1-3 sentences and move it to the archive at the bottom. Reference this file when asked about competitor moves, EdTech trends, or strategic action items.
|
## Week of January 26 - February 1, 2026
|
||||||
|
|
||||||
|
### 7Sage
|
||||||
|
- **New Promo:** $10,000 giveaway contest for students who track applications via "My Schools" feature (deadline Jan 30, 2026)
|
||||||
|
- **Site Update:** New mobile-friendly design; app in development but no release date announced
|
||||||
|
- **Pricing:** Core ($69/mo), Live ($129/mo), Coach ($299/mo) - all require $120/yr LSAC LawHub Advantage
|
||||||
|
- **Key Differentiator:** "Insanely granular" test analytics, 924 video lessons, 7,500+ Reddit upvotes mentioned
|
||||||
|
|
||||||
|
### LSAT Demon
|
||||||
|
- **Pricing:** $95/month (lowest among major competitors)
|
||||||
|
- **Live Classes:** Daily Zoom-based classes covering all LSAT sections
|
||||||
|
- **Content:** 10,000+ explanations, official LSAT drilling, "Ask" feature with 24-hour response time
|
||||||
|
- **Founders:** Ben Olson & Nathan Fox (Thinking LSAT Podcast)
|
||||||
|
|
||||||
|
### Blueprint LSAT
|
||||||
|
- **Focus:** Heavy emphasis on 1:1 private tutoring
|
||||||
|
- **Social Proof:** Numerous 5-star reviews citing 9+ point score improvements
|
||||||
|
- **Notable:** Strong instructor personalization mentioned in reviews (Bobby, Dylan, Hannah, Larissa)
|
||||||
|
|
||||||
|
### PowerScore
|
||||||
|
- **Dave Killoran departed** (HUGE personnel change) - Jon Denning continuing solo
|
||||||
|
- **MAJOR UPDATE:** New 2025-2026 products include "The Law School Admissions Bible" written by Spivey Consulting
|
||||||
|
- **Strategic Pivot:** Moving beyond pure LSAT prep into law school admissions consulting space (reinforced Jan 30)
|
||||||
|
- **Live Classes:** Extensive daily schedule with topic-specific sessions (RC, LR, LG question types)
|
||||||
|
- **Strategic read:** Industry veteran leaving created uncertainty, but new Spivey partnership signals aggressive admissions push. Burton's visual/multimodal approach could differentiate in market where LSAT alone matters less.
|
||||||
|
|
||||||
|
### Magoosh LSAT
|
||||||
|
- **Content:** Active blog with study resources, percentile calculators
|
||||||
|
- **Less Visible:** Homepage minimal; main value appears to be blog content
|
||||||
|
|
||||||
|
### LSAC Official
|
||||||
|
- **January 2026 LSAT:** Score release 1/28/2026
|
||||||
|
- **Testing Disruption:** Mainland China testing unavailable for January 2026 LSAT
|
||||||
|
- **International:** International administration available, but China exception notable
|
||||||
|
|
||||||
|
### Industry Trends (EdCircuit, Buffalo Law)
|
||||||
|
- **Rising Competition:** Law school applications and LSAT scores both increasing for 2025-2026 cycle
|
||||||
|
- **Diminishing Differentiation:** High LSAT scores becoming less differentiating in applicant pools (reinforced Jan 30)
|
||||||
|
- **Access Issue:** Financial backing and unpaid prep time creating advantages for wealthy students
|
||||||
|
- **Jan 30 Update:** Applications + LSAT scores both rising → high scores becoming less differentiating. Need efficiency and unique methodology to stand out.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📊 Current Week Intel (Week of Jan 27 - Feb 2, 2026)
|
### Blogwatcher Status
|
||||||
|
- **13 feeds tracked**, but most competitor blogs lack working RSS/scrapers
|
||||||
|
- **No new articles this week** (as of Jan 30)
|
||||||
|
- **Action needed:** Manually configure or find alternative data sources for key competitor blogs
|
||||||
|
|
||||||
### 🚨 CRITICAL: LSAC Format Change
|
## Previous Weeks
|
||||||
**Reading Comp removed comparative passages** in January 2026 administration. Confirmed by Blueprint and PowerScore.
|
|
||||||
- **Impact:** Any RC curriculum teaching comparative passage strategy is now outdated
|
|
||||||
- **Opportunity:** First to fully adapt = trust signal to students
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Competitor Movements
|
|
||||||
|
|
||||||
**7Sage**
|
|
||||||
- Full site redesign launched (better analytics, cleaner UI)
|
|
||||||
- **NEW FREE FEATURE:** Application tracker showing interview/accept/reject/waitlist outcomes
|
|
||||||
- $10,000 giveaway promotion tied to tracker
|
|
||||||
- Heavy ABA 509 report coverage
|
|
||||||
- ADHD accommodations content series + 1L survival guides
|
|
||||||
- **Pricing (Jan 27):** Core $69/mo | Live $129/mo | Coach $299/mo (all require LawHub $120/yr)
|
|
||||||
- **Scale:** 60+ live classes/week, 3,000+ recorded classes
|
|
||||||
- **Strategic read:** Pushing hard into admissions territory, not just LSAT. Creates stickiness + data network effects.
|
|
||||||
|
|
||||||
**LSAT Demon**
|
|
||||||
- **"Ugly Mode"** (Jan 19) — transforms interface to match exact official LSAT layout
|
|
||||||
- Tuition Roll Call on scholarship estimator — visualizes what students actually paid
|
|
||||||
- Veteran outreach program with dedicated liaison
|
|
||||||
- **Pricing (Jan 27):** Entry at $95/month — competitive with 7Sage Core
|
|
||||||
- **Strategic read:** Daily podcast creates parasocial relationships. Demon is personality-driven; Burton is methodology-driven. Different lanes.
|
|
||||||
|
|
||||||
**PowerScore**
|
|
||||||
- **Dave Killoran departed** (HUGE personnel change)
|
|
||||||
- Jon Denning continuing solo, covering January LSAT chaos extensively
|
|
||||||
- Crystal Ball webinars still running
|
|
||||||
- **Strategic read:** Industry veteran leaving creates uncertainty. Watch for quality/content changes.
|
|
||||||
|
|
||||||
**Blueprint**
|
|
||||||
- First to report RC comparative passages removal
|
|
||||||
- Non-traditional student content (LSAT at 30/40/50+)
|
|
||||||
- Score plateau breakthrough guides
|
|
||||||
- 2025-26 admissions cycle predictions
|
|
||||||
- **Jan 27 Update:** Heavy push on 1:1 tutoring testimonials; "170+ course" positioning; reviews emphasizing "plateau breakthroughs" and test anxiety management
|
|
||||||
- **Strategic read:** Solid content machine, "fun" brand positioning. Going premium/high-touch with tutoring.
|
|
||||||
|
|
||||||
**Kaplan**
|
|
||||||
- $200 off all LSAT prep **extended through Jan 26** (expires TODAY)
|
|
||||||
- Applies to On Demand, Live Online, In Person, and Standard Tutoring
|
|
||||||
- Bar prep also discounted ($750 off through Feb 27)
|
|
||||||
- New 2026 edition book with "99th percentile instructor videos"
|
|
||||||
- **Strategic read:** Mass-market, price-conscious positioning continues. Heavy discounting signals competitive pressure.
|
|
||||||
|
|
||||||
**Magoosh**
|
|
||||||
- Updated for post-Logic Games LSAT
|
|
||||||
- Budget positioning continues
|
|
||||||
- LSAC remote proctoring option coverage
|
|
||||||
- **Jan 28 Update:** Landing page now shows blog-only content — no active LSAT prep product visible. Appears to have scaled back or exited market. Potential market consolidation signal.
|
|
||||||
|
|
||||||
**LSAC (Official)**
|
|
||||||
- February 2026 scheduling opened Jan 20
|
|
||||||
- January registration closed; score release Jan 28
|
|
||||||
- Mainland China testing unavailable for Jan 2026
|
|
||||||
- Reminder to disable grammar-checking programs for Argumentative Writing
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### EdTech Trends (Jan 27 Scan)
|
|
||||||
|
|
||||||
| Story | Score | Key Insight |
|
|
||||||
|-------|-------|-------------|
|
|
||||||
| **AdeptLR: LSAT AI Competitor** | 9/10 | Direct LSAT LR adaptive AI platform; validates market, narrow focus (LR only) |
|
|
||||||
| **Blueprint: AI Could Mess Up LSAT Prep** | 8/10 | GPT-4 scored 163; AI gives confident wrong answers; training data outdated |
|
|
||||||
| **UB Law: Apps Up 22%** | 7/10 | Highest applicant volume in decade; 160-180 scores climbing; career changers entering |
|
|
||||||
| **TCS: EdTech Trends 2026** | 7/10 | AI adaptive models proven; "agentic AI" emerging; outcomes > features now |
|
|
||||||
| **LSAC: 2026 Cycle Strong** | 7/10 | Official confirmation LSAT volumes high; broad applicant base |
|
|
||||||
| **eSchool: 49 EdTech Predictions** | 6/10 | "Shiny AI era over" — measurable outcomes required to win |
|
|
||||||
|
|
||||||
### Previous Scan (Jan 25)
|
|
||||||
|
|
||||||
| Story | Score | Key Insight |
|
|
||||||
|-------|-------|-------------|
|
|
||||||
| AI Can Deepen Learning | 8/10 | AI mistakes spark deeper learning; productive friction > shortcuts |
|
|
||||||
| Beyond Memorization: Redefining Rigor | 8/10 | LSAT-relevant: adaptability + critical thinking > memorization |
|
|
||||||
| Teaching Machines to Spot Human Errors | 7/10 | Eedi Labs predicting student misconceptions; human-in-the-loop AI tutoring |
|
|
||||||
| Learning As Addictive As TikTok? 7/10 | Dopamine science for engagement; make progress feel attainable |
|
|
||||||
| What Students Want From Edtech | 6/10 | UX research: clarity > gimmicks; meaningful gamification only |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📌 Identified Action Items
|
|
||||||
|
|
||||||
0. **🚨 TIME-SENSITIVE (Jan 28):** January LSAT scores release TODAY — prime acquisition window for students who missed target. Consider outreach campaign for 1/28-1/31.
|
|
||||||
1. **URGENT:** Update RC content to remove/deprioritize comparative passage strategy
|
|
||||||
2. **Content opportunity:** Blog post "What the RC Changes Mean for Your Score" — be fast, be definitive
|
|
||||||
3. **Positioning clarity:** 7Sage → admissions features, Demon → personality, Burton → systematic methodology that transcends format changes
|
|
||||||
4. **Product opportunity:** Consider "productive friction" AI features that make students think, not just answer
|
|
||||||
5. **Watch:** PowerScore post-Killoran quality — potential talent acquisition or market share opportunity
|
|
||||||
6. **NEW - Competitive research:** Study AdeptLR UX for adaptive LSAT AI patterns (what works, what doesn't)
|
|
||||||
7. **NEW - Differentiation angle:** Build AI that admits uncertainty (address Blueprint's "confident wrong answers" critique)
|
|
||||||
8. **NEW - Marketing:** Track & publish score improvement data to meet "outcomes over features" bar
|
|
||||||
9. **NEW - Market validation:** 22% app surge + decade-high volumes = sustained demand; career changers = self-study friendly audience
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Previous Weeks Archive
|
|
||||||
|
|
||||||
*(No previous weeks yet — this section will grow as weeks pass)*
|
|
||||||
|
|
||||||
|
*No prior weeks tracked - file created 2026-01-30*
|
||||||
|
|||||||
@ -10,3 +10,5 @@
|
|||||||
2026-01-26: Make today count! Pickle moment: Why are pickles such good friends? They're always there when you're in a jam...or jar.
|
2026-01-26: Make today count! Pickle moment: Why are pickles such good friends? They're always there when you're in a jam...or jar.
|
||||||
2026-01-27: Your time is now! Pickles are wild: What's a pickle's favorite day of the week? Fri-dill of course.
|
2026-01-27: Your time is now! Pickles are wild: What's a pickle's favorite day of the week? Fri-dill of course.
|
||||||
2026-01-28: You're unstoppable! Quick pickle story: Why are pickles so resilient? They've been through a lot - literally submerged and came out crunchier.
|
2026-01-28: You're unstoppable! Quick pickle story: Why are pickles so resilient? They've been through a lot - literally submerged and came out crunchier.
|
||||||
|
2026-01-29: Dream big, work hard! Pickle knowledge drop: What do you call a pickle that's really stressed? A dill-lemma.
|
||||||
|
2026-01-30: Consistency is key! Never forget about pickles: Why did the pickle go to therapy? It had some unresolved jar issues.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user