diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..3c032078 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/CLAUDE-DESKTOP-DEPLOYMENT-PLAN.md b/CLAUDE-DESKTOP-DEPLOYMENT-PLAN.md deleted file mode 100644 index 4eabecb9..00000000 --- a/CLAUDE-DESKTOP-DEPLOYMENT-PLAN.md +++ /dev/null @@ -1,826 +0,0 @@ -# 🚀 Claude Desktop + GoHighLevel MCP Server Deployment Plan - -> **GOAL**: Deploy your GoHighLevel MCP server to work flawlessly with Claude Desktop, giving Claude access to all 21 GoHighLevel tools for contact management, messaging, and blog operations. - -## 🔴 **CRITICAL CORRECTIONS APPLIED** - -This deployment plan has been **thoroughly reviewed and corrected** to ensure 100% accuracy: - -✅ **Fixed cloud deployment strategy** - Removed incorrect `mcp-remote` SSE configuration -✅ **Added Windows support** - Complete file paths and commands for Windows users -✅ **Clarified STDIO vs HTTP servers** - Proper usage for Claude Desktop vs cloud deployment -✅ **Corrected package.json publishing** - Added proper build sequence for NPM packages -✅ **Fixed Railway/Cloud Run configs** - Using HTTP server for cloud platforms with health checks -✅ **Added platform-specific paths** - macOS and Windows configuration file locations - -**All configurations have been verified against official MCP documentation and Claude Desktop requirements.** - -### 📊 **SERVER TYPE REFERENCE** - -| Use Case | Server Type | Command | Protocol | -|----------|-------------|---------|----------| -| **Claude Desktop** | STDIO | `node dist/server.js` | STDIO Transport | -| **Cloud Deployment** | HTTP | `node dist/http-server.js` | HTTP/SSE Transport | -| **NPM Package** | STDIO | Via npx | STDIO Transport | -| **Docker (Claude Desktop)** | STDIO | Override CMD | STDIO Transport | -| **Docker (Cloud)** | HTTP | Default CMD | HTTP/SSE Transport | - -## 📋 Executive Summary - -This plan provides **5 deployment strategies** ranging from simple local setup to enterprise-grade cloud deployment, each optimized for Claude Desktop's STDIO-based MCP protocol requirements. - -**Current Status**: ✅ Your GHL MCP server is production-ready with 21 tools -**Target**: 🎯 Flawless Claude Desktop integration with reliable server access -**Timeline**: 🕐 15 minutes (local) to 2 hours (full cloud deployment) - ---- - -## 🎯 **STRATEGY 1: LOCAL DEVELOPMENT (FASTEST - 15 minutes)** - -### Why This First? -- **Immediate testing** - Verify everything works before cloud deployment -- **Zero cost** - No hosting fees during development -- **Full debugging** - Complete control over logs and configuration -- **Perfect for development** - Rapid iteration and testing - -### Step-by-Step Implementation - -#### 1. Environment Setup -```bash -# 1. Clone and setup (if not already done) -cd /path/to/ghl-mcp-server -npm install -npm run build - -# 2. Create environment file -cat > .env << EOF -GHL_API_KEY=your_ghl_api_key_here -GHL_BASE_URL=https://services.leadconnectorhq.com -GHL_LOCATION_ID=your_location_id_here -NODE_ENV=development -EOF -``` - -#### 2. Test Server Locally -```bash -# Test the MCP server directly -npm run start:stdio - -# You should see: -# 🚀 Starting GoHighLevel MCP Server... -# ✅ GoHighLevel MCP Server started successfully! -# 📋 Available tools: 21 -``` - -#### 3. Configure Claude Desktop - -**Configuration File Locations**: -- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` -- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` - -**macOS Configuration**: -```json -{ - "mcpServers": { - "ghl-mcp-local": { - "command": "node", - "args": ["/Users/YOUR_USERNAME/path/to/ghl-mcp-server/dist/server.js"], - "env": { - "GHL_API_KEY": "your_ghl_api_key_here", - "GHL_BASE_URL": "https://services.leadconnectorhq.com", - "GHL_LOCATION_ID": "your_location_id_here" - } - } - } -} -``` - -**Windows Configuration**: -```json -{ - "mcpServers": { - "ghl-mcp-local": { - "command": "node", - "args": ["C:\\Users\\YOUR_USERNAME\\path\\to\\ghl-mcp-server\\dist\\server.js"], - "env": { - "GHL_API_KEY": "your_ghl_api_key_here", - "GHL_BASE_URL": "https://services.leadconnectorhq.com", - "GHL_LOCATION_ID": "your_location_id_here" - } - } - } -} -``` - -#### 4. Test Claude Desktop Integration -1. Restart Claude Desktop completely -2. Look for 🔨 tools icon in bottom-right -3. Test with: *"List my GoHighLevel contacts"* - -### ✅ Success Criteria -- Claude Desktop shows tools icon -- All 21 GHL tools are available -- Can execute contact searches, send messages, create blog posts - ---- - -## 🎯 **STRATEGY 2: NPM PACKAGE DEPLOYMENT (RECOMMENDED - 30 minutes)** - -### Why This Approach? -- **Global accessibility** - Install anywhere with one command -- **Version control** - Easy updates and rollbacks -- **Professional distribution** - Standard Node.js ecosystem -- **Claude Desktop friendly** - Perfect for npx integration - -### Implementation Steps - -#### 1. Prepare for NPM Publishing -```bash -# Update package.json for NPM -cat > package.json << 'EOF' -{ - "name": "@yourusername/ghl-mcp-server", - "version": "1.0.0", - "description": "GoHighLevel MCP Server for Claude Desktop", - "main": "dist/server.js", - "bin": { - "ghl-mcp-server": "dist/server.js" - }, - "scripts": { - "build": "tsc", - "start": "node dist/server.js", - "prepublishOnly": "npm run build" - }, - "keywords": ["mcp", "gohighlevel", "claude", "ai"], - "files": ["dist/", "README.md", "package.json"], - "engines": { - "node": ">=18.0.0" - } -} -EOF - -# Add shebang to built server.js (IMPORTANT: Do this AFTER npm run build) -npm run build -echo '#!/usr/bin/env node' | cat - dist/server.js > temp && mv temp dist/server.js -chmod +x dist/server.js -``` - -#### 2. Publish to NPM -```bash -# Login to NPM (one time setup) -npm login - -# Publish package -npm publish --access public -``` - -#### 3. Configure Claude Desktop (NPM Version) -```json -{ - "mcpServers": { - "ghl-mcp-npm": { - "command": "npx", - "args": ["-y", "@yourusername/ghl-mcp-server"], - "env": { - "GHL_API_KEY": "your_ghl_api_key_here", - "GHL_BASE_URL": "https://services.leadconnectorhq.com", - "GHL_LOCATION_ID": "your_location_id_here" - } - } - } -} -``` - -### ✅ Benefits -- ✅ **Easy distribution** - Share with team via package name -- ✅ **Auto-updates** - `npx` always gets latest version -- ✅ **No local setup** - Works on any machine with Node.js -- ✅ **Version pinning** - Can specify exact versions if needed - ---- - -## 🎯 **STRATEGY 3: DOCKER CONTAINERIZED DEPLOYMENT (45 minutes)** - -### Why Docker? -- **Environment isolation** - No dependency conflicts -- **Consistent deployment** - Same environment everywhere -- **Easy scaling** - Container orchestration ready -- **Production proven** - Industry standard for deployment - -### Implementation Steps - -#### 1. Optimize Dockerfile for MCP -```dockerfile -# Create optimized Dockerfile -cat > Dockerfile << 'EOF' -FROM node:18-alpine - -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Install dependencies -RUN npm ci --only=production - -# Copy built application -COPY dist/ ./dist/ -COPY .env.example ./ - -# Create non-root user -RUN addgroup -g 1001 -S nodejs && \ - adduser -S ghl-mcp -u 1001 -G nodejs - -USER ghl-mcp - -EXPOSE 8000 - -# Default to HTTP server for cloud deployment -# For Claude Desktop STDIO, override: docker run ... yourusername/ghl-mcp-server node dist/server.js -CMD ["node", "dist/http-server.js"] -EOF -``` - -#### 2. Build and Test Container -```bash -# Build Docker image -docker build -t ghl-mcp-server . - -# Test container locally -docker run -it --rm \ - -e GHL_API_KEY="your_api_key" \ - -e GHL_LOCATION_ID="your_location_id" \ - ghl-mcp-server -``` - -#### 3. Deploy to Container Registry -```bash -# Push to Docker Hub -docker tag ghl-mcp-server yourusername/ghl-mcp-server:latest -docker push yourusername/ghl-mcp-server:latest - -# Or GitHub Container Registry -docker tag ghl-mcp-server ghcr.io/yourusername/ghl-mcp-server:latest -docker push ghcr.io/yourusername/ghl-mcp-server:latest -``` - -#### 4. Claude Desktop Configuration (Docker) -```json -{ - "mcpServers": { - "ghl-mcp-docker": { - "command": "docker", - "args": [ - "run", "--rm", "-i", - "-e", "GHL_API_KEY=your_api_key", - "-e", "GHL_LOCATION_ID=your_location_id", - "yourusername/ghl-mcp-server:latest" - ] - } - } -} -``` - -### ✅ Advanced Docker Features -- **Health checks** - Monitor server status -- **Volume mounts** - Persistent configuration -- **Multi-stage builds** - Smaller production images -- **Security scanning** - Vulnerability detection - ---- - -## 🎯 **STRATEGY 4: CLOUD DEPLOYMENT WITH REMOTE ACCESS (60 minutes)** - -### Why Cloud Deployment? -- **24/7 availability** - Always accessible to Claude Desktop -- **Team sharing** - Multiple users can access same server -- **Scalability** - Handle increased usage automatically -- **Monitoring** - Built-in logging and metrics - -### Option 4A: Railway Deployment - -#### 1. Railway Setup -```bash -# Install Railway CLI -npm install -g @railway/cli - -# Login and deploy -railway login -railway init -railway up -``` - -#### 2. Configure Environment Variables -```bash -# Set via Railway CLI -railway variables set GHL_API_KEY=your_api_key -railway variables set GHL_LOCATION_ID=your_location_id -railway variables set NODE_ENV=production -``` - -#### 3. Create Railway Configuration -```json -// railway.json -{ - "build": { - "builder": "DOCKERFILE" - }, - "deploy": { - "startCommand": "npm run start:http", - "healthcheckPath": "/health" - } -} -``` - -**⚠️ IMPORTANT**: For cloud deployment, use the HTTP server (`npm run start:http`) which provides the `/health` endpoint. The STDIO server is for direct Claude Desktop connections only. - -### Option 4B: Google Cloud Run Deployment - -#### 1. Build and Deploy -```bash -# Configure gcloud -gcloud auth login -gcloud config set project your-project-id - -# Build and deploy -gcloud builds submit --tag gcr.io/your-project-id/ghl-mcp-server -gcloud run deploy ghl-mcp-server \ - --image gcr.io/your-project-id/ghl-mcp-server \ - --platform managed \ - --region us-central1 \ - --set-env-vars="GHL_API_KEY=your_api_key,GHL_LOCATION_ID=your_location_id" -``` - -#### 2. Configure for HTTP Server (Required for Cloud Run) -```yaml -# cloud-run-service.yaml -apiVersion: serving.knative.dev/v1 -kind: Service -metadata: - name: ghl-mcp-server -spec: - template: - spec: - containers: - - image: gcr.io/your-project-id/ghl-mcp-server - command: ["node", "dist/http-server.js"] - ports: - - containerPort: 8000 - env: - - name: GHL_API_KEY - value: "your_api_key" - - name: GHL_LOCATION_ID - value: "your_location_id" - - name: PORT - value: "8000" -``` - -**⚠️ IMPORTANT**: Cloud platforms require HTTP servers with health endpoints. Use `dist/http-server.js` for cloud deployment, not `dist/server.js` (STDIO version). - -### Claude Desktop Configuration (Cloud) - -**⚠️ IMPORTANT**: Claude Desktop requires STDIO transport, not HTTP/SSE. For cloud deployment with Claude Desktop, you have three options: - -#### Option A: SSH Tunnel to Cloud Instance -```json -{ - "mcpServers": { - "ghl-mcp-cloud": { - "command": "ssh", - "args": [ - "your-server", - "cd /path/to/ghl-mcp-server && node dist/server.js" - ], - "env": { - "GHL_API_KEY": "your_api_key", - "GHL_LOCATION_ID": "your_location_id" - } - } - } -} -``` - -#### Option B: Remote Docker Container -```json -{ - "mcpServers": { - "ghl-mcp-cloud": { - "command": "docker", - "args": [ - "run", "--rm", "-i", - "-e", "GHL_API_KEY=your_api_key", - "-e", "GHL_LOCATION_ID=your_location_id", - "your-registry/ghl-mcp-server:latest" - ] - } - } -} -``` - -#### Option C: Local NPM with Remote Dependencies -```json -{ - "mcpServers": { - "ghl-mcp-cloud": { - "command": "npx", - "args": ["-y", "@yourusername/ghl-mcp-server"], - "env": { - "GHL_API_KEY": "your_api_key", - "GHL_LOCATION_ID": "your_location_id", - "GHL_BASE_URL": "https://services.leadconnectorhq.com" - } - } - } -} -``` - ---- - -## 🎯 **STRATEGY 5: ENTERPRISE PRODUCTION DEPLOYMENT (120 minutes)** - -### Why Enterprise Approach? -- **High availability** - 99.9% uptime guarantees -- **Security hardening** - Enterprise-grade protection -- **Monitoring & alerting** - Comprehensive observability -- **Compliance ready** - Audit trails and data governance - -### Architecture Overview -```mermaid -graph TB - A[Claude Desktop] --> B[Load Balancer] - B --> C[MCP Server Cluster] - C --> D[GoHighLevel API] - C --> E[Redis Cache] - C --> F[Monitoring Stack] - G[CI/CD Pipeline] --> C -``` - -### Implementation Components - -#### 1. Infrastructure as Code (Terraform) -```hcl -# infrastructure/main.tf -resource "aws_ecs_service" "ghl_mcp_server" { - name = "ghl-mcp-server" - cluster = aws_ecs_cluster.main.id - task_definition = aws_ecs_task_definition.ghl_mcp.arn - desired_count = 3 - - deployment_configuration { - maximum_percent = 200 - minimum_healthy_percent = 100 - } - - load_balancer { - target_group_arn = aws_lb_target_group.ghl_mcp.arn - container_name = "ghl-mcp-server" - container_port = 8000 - } -} -``` - -#### 2. Kubernetes Deployment -```yaml -# k8s/deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ghl-mcp-server -spec: - replicas: 3 - selector: - matchLabels: - app: ghl-mcp-server - template: - metadata: - labels: - app: ghl-mcp-server - spec: - containers: - - name: ghl-mcp-server - image: ghl-mcp-server:latest - ports: - - containerPort: 8000 - envFrom: - - secretRef: - name: ghl-mcp-secrets - resources: - limits: - memory: "512Mi" - cpu: "500m" - requests: - memory: "256Mi" - cpu: "250m" - livenessProbe: - httpGet: - path: /health - port: 8000 - initialDelaySeconds: 30 - periodSeconds: 10 -``` - -#### 3. Monitoring & Observability -```yaml -# monitoring/prometheus-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: prometheus-config -data: - prometheus.yml: | - global: - scrape_interval: 15s - scrape_configs: - - job_name: 'ghl-mcp-server' - static_configs: - - targets: ['ghl-mcp-server:8000'] - metrics_path: /metrics -``` - -#### 4. Security Configuration -```yaml -# security/network-policy.yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: ghl-mcp-network-policy -spec: - podSelector: - matchLabels: - app: ghl-mcp-server - policyTypes: - - Ingress - - Egress - ingress: - - from: - - namespaceSelector: - matchLabels: - name: claude-desktop - ports: - - protocol: TCP - port: 8000 -``` - ---- - -## 🔧 **TROUBLESHOOTING GUIDE** - -### Common Issues & Solutions - -#### Issue 1: Claude Desktop Not Detecting Server -**Symptoms**: No tools icon, server not connecting - -**Solutions**: -```bash -# Check server logs (macOS) -tail -f ~/Library/Logs/Claude/mcp*.log - -# Check server logs (Windows) -type %APPDATA%\Claude\Logs\mcp*.log - -# Verify configuration syntax (macOS) -cat ~/Library/Application\ Support/Claude/claude_desktop_config.json | jq . - -# Verify configuration syntax (Windows) -type %APPDATA%\Claude\claude_desktop_config.json - -# Test server manually -node /path/to/ghl-mcp-server/dist/server.js -``` - -#### Issue 2: GHL API Authentication Fails -**Symptoms**: "Invalid API key" or "Unauthorized" errors - -**Solutions**: -```bash -# Test API key directly -curl -H "Authorization: Bearer $GHL_API_KEY" \ - https://services.leadconnectorhq.com/locations/ - -# Verify environment variables -echo $GHL_API_KEY -echo $GHL_LOCATION_ID -``` - -#### Issue 3: Server Process Crashes -**Symptoms**: Server starts then immediately exits - -**Solutions**: -```bash -# Check for missing dependencies -npm install - -# Verify Node.js version -node --version # Should be 18+ - -# Run with debug logging -DEBUG=* node dist/server.js -``` - -#### Issue 4: Tool Execution Timeouts -**Symptoms**: Tools start but never complete - -**Solutions**: -```javascript -// Add timeout configuration -const client = new GHLApiClient({ - timeout: 30000, // 30 second timeout - retries: 3 // Retry failed requests -}); -``` - -### Health Check Commands -```bash -# Test server health -curl http://localhost:8000/health - -# List available tools -curl http://localhost:8000/tools - -# Test MCP protocol -echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/server.js -``` - ---- - -## 📊 **MONITORING & MAINTENANCE** - -### Key Metrics to Track -1. **Server Uptime** - Target: 99.9% -2. **API Response Times** - Target: <2 seconds -3. **Tool Success Rate** - Target: >95% -4. **Memory Usage** - Target: <512MB -5. **CPU Utilization** - Target: <50% - -### Automated Monitoring Setup -```bash -# Install monitoring tools -npm install prometheus-client - -# Add to server.js -const promClient = require('prom-client'); -const register = new promClient.Registry(); - -// Create metrics -const toolExecutionDuration = new promClient.Histogram({ - name: 'ghl_mcp_tool_duration_seconds', - help: 'Duration of tool executions', - labelNames: ['tool_name', 'status'] -}); - -register.registerMetric(toolExecutionDuration); -``` - -### Backup & Recovery -```bash -# Backup configuration (macOS) -cp ~/Library/Application\ Support/Claude/claude_desktop_config.json \ - ~/Desktop/claude_config_backup_$(date +%Y%m%d).json - -# Backup configuration (Windows) -copy "%APPDATA%\Claude\claude_desktop_config.json" "%USERPROFILE%\Desktop\claude_config_backup_%date:~-4,4%%date:~-10,2%%date:~-7,2%.json" - -# Export environment variables (macOS/Linux) -printenv | grep GHL_ > ghl_env_backup.txt - -# Export environment variables (Windows) -set | findstr GHL_ > ghl_env_backup.txt -``` - ---- - -## 🚀 **RECOMMENDED IMPLEMENTATION PATH** - -### Phase 1: Quick Start (Day 1) -1. ✅ **Strategy 1**: Local development setup -2. ✅ Test all 21 tools with Claude Desktop -3. ✅ Verify GoHighLevel API connectivity -4. ✅ Document working configuration - -### Phase 2: Distribution (Day 2-3) -1. ✅ **Strategy 2**: NPM package deployment -2. ✅ Set up CI/CD pipeline for automated builds -3. ✅ Create comprehensive documentation -4. ✅ Test installation on clean systems - -### Phase 3: Production (Week 2) -1. ✅ **Strategy 4**: Cloud deployment -2. ✅ Implement monitoring and alerting -3. ✅ Set up backup and recovery procedures -4. ✅ Performance optimization and scaling - -### Phase 4: Enterprise (Month 2) -1. ✅ **Strategy 5**: Enterprise deployment -2. ✅ Security hardening and compliance -3. ✅ Advanced monitoring and analytics -4. ✅ Multi-region deployment for redundancy - ---- - -## 💡 **OPTIMIZATION TIPS** - -### Performance Optimization -```typescript -// Add connection pooling -const apiClient = new GHLApiClient({ - maxConnections: 10, - keepAlive: true, - timeout: 15000 -}); - -// Implement caching -const cache = new Map(); -const getCachedResult = (key: string, fetcher: Function) => { - if (cache.has(key)) return cache.get(key); - const result = fetcher(); - cache.set(key, result); - return result; -}; -``` - -### Security Enhancements -```bash -# Use environment-specific configurations -NODE_ENV=production npm start - -# Implement API key rotation -echo "0 2 * * * /usr/local/bin/rotate-ghl-keys.sh" | crontab - - -# Add request rate limiting -npm install express-rate-limit -``` - -### Scalability Considerations -```yaml -# Horizontal pod autoscaler -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: ghl-mcp-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: ghl-mcp-server - minReplicas: 2 - maxReplicas: 10 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 -``` - ---- - -## ✅ **SUCCESS VALIDATION CHECKLIST** - -### Claude Desktop Integration -- [ ] Tools icon appears in Claude Desktop -- [ ] All 21 GHL tools are listed and accessible -- [ ] Can create, search, and update contacts -- [ ] Can send SMS and email messages -- [ ] Can manage blog posts and content -- [ ] Server responds within 5 seconds -- [ ] No timeout errors during normal operation - -### Production Readiness -- [ ] Server starts reliably on system boot -- [ ] Handles API rate limits gracefully -- [ ] Logs all operations for debugging -- [ ] Monitoring alerts are configured -- [ ] Backup procedures are documented -- [ ] Security best practices implemented - -### Performance Benchmarks -- [ ] Handles 100+ concurrent requests -- [ ] Memory usage remains under 512MB -- [ ] API response times under 2 seconds -- [ ] 99% uptime over 30 days -- [ ] Zero data loss incidents - ---- - -## 🎯 **NEXT STEPS** - -1. **Choose your deployment strategy** based on your needs: - - **Local development**: Strategy 1 - - **Team sharing**: Strategy 2 (NPM) - - **Production deployment**: Strategy 4 (Cloud) - - **Enterprise scale**: Strategy 5 - -2. **Follow the step-by-step guide** for your chosen strategy - -3. **Test thoroughly** with the provided validation checklist - -4. **Monitor and optimize** using the recommended tools and metrics - -5. **Scale up** as your usage grows and requirements evolve - -**You're now ready to give Claude Desktop superpowers with your GoHighLevel integration!** 🚀 - ---- - -*This deployment plan ensures your GHL MCP server works flawlessly with Claude Desktop across all environments, from local development to enterprise production.* \ No newline at end of file diff --git a/README-GITHUB.md b/README-GITHUB.md index bddfe7f7..93848ec7 100644 --- a/README-GITHUB.md +++ b/README-GITHUB.md @@ -1,3 +1,4 @@ +@ -1,249 +0,0 @@ # 🚀 GoHighLevel MCP Server [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/mastanley13/GoHighLevel-MCP) diff --git a/TEST-VAPI-MCP.md b/TEST-VAPI-MCP.md new file mode 100644 index 00000000..be20d86f --- /dev/null +++ b/TEST-VAPI-MCP.md @@ -0,0 +1,121 @@ +# 🔍 Test Vapi MCP Connection + +## Step-by-Step Debugging + +### Test 1: Health Check ✅ +```bash +curl https://gohighlevel-mcp-vapi.up.railway.app/health +``` + +**Expected:** Should show `"server": "vapi-ghl-mcp-server"` + +### Test 2: MCP Initialize Endpoint +```bash +curl -X POST https://gohighlevel-mcp-vapi.up.railway.app/mcp/initialize \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "vapi", + "version": "1.0.0" + } + } + }' +``` + +**Expected Response:** +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": "vapi-ghl-mcp-server", + "version": "1.0.0" + } + } +} +``` + +### Test 3: MCP Tools List +```bash +curl -X POST https://gohighlevel-mcp-vapi.up.railway.app/mcp/tools/list \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list" + }' +``` + +**Expected:** Should return 221 tools + +### Test 4: Test a Simple Tool Call +```bash +curl -X POST https://gohighlevel-mcp-vapi.up.railway.app/mcp/tools/call \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "search_contacts", + "arguments": { + "query": "test", + "limit": 1 + } + } + }' +``` + +## Common Issues + +### Issue 1: Port Mismatch +Railway might be using a different PORT than 8001. + +**Fix:** Ensure server uses Railway's PORT env var (already done in code) + +### Issue 2: CORS Headers +Vapi needs proper CORS headers. + +**Fix:** Already configured in vapi-mcp-server.ts + +### Issue 3: Wrong URL in Vapi +Make sure Vapi is configured with the ROOT URL. + +**Correct:** +```json +{ + "server": { + "url": "https://gohighlevel-mcp-vapi.up.railway.app" + } +} +``` + +**Wrong:** +```json +{ + "server": { + "url": "https://gohighlevel-mcp-vapi.up.railway.app/mcp" + } +} +``` + +### Issue 4: Protocol Mismatch +Vapi expects Streamable HTTP by default. + +**Check:** Make sure you're NOT setting `"protocol": "sse"` in Vapi config + +## Run These Tests + +Please run Test 1, 2, and 3 and share the results! + diff --git a/VAPI-INTEGRATION-GUIDE.md b/VAPI-INTEGRATION-GUIDE.md new file mode 100644 index 00000000..c771f654 --- /dev/null +++ b/VAPI-INTEGRATION-GUIDE.md @@ -0,0 +1,273 @@ +# 🎤 Vapi AI Integration Guide - GoHighLevel MCP Server + +This guide shows you how to integrate the GoHighLevel MCP Server with Vapi AI using the **Streamable HTTP** protocol. + +## 🔧 **Why a Separate Server?** + +Vapi uses **Streamable HTTP** protocol by default, not SSE (Server-Sent Events). The original `http-server.ts` uses SSE which causes 500 errors in Vapi. The new `vapi-mcp-server.ts` is specifically designed for Vapi compatibility. + +## 📋 **Prerequisites** + +- Active GoHighLevel account with API access +- Vapi AI account +- Railway, Vercel, or similar deployment platform +- Your GHL Location ID and API Key + +## 🚀 **Step 1: Deploy the Vapi MCP Server** + +### Option A: Railway Deployment (Recommended) + +1. **Fork/Clone this repository** +2. **Connect to Railway** +3. **Set Environment Variables:** + ```env + GHL_API_KEY=your_ghl_api_key_here + GHL_LOCATION_ID=your_location_id_here + GHL_BASE_URL=https://services.leadconnectorhq.com + VAPI_MCP_PORT=8001 + ``` + +4. **Set Start Command:** + ``` + npm run start:vapi + ``` + +5. **Deploy and Get URL:** + - Railway will provide a URL like: `https://your-app.up.railway.app` + - Test health: `https://your-app.up.railway.app/health` + +### Option B: Vercel Deployment + +1. **Deploy to Vercel** +2. **Configure Build Settings:** + - Build Command: `npm run build` + - Output Directory: `dist` + - Install Command: `npm install` + +3. **Add Environment Variables** (same as Railway) +4. **Create `vercel.json`:** + ```json + { + "version": 2, + "builds": [ + { + "src": "dist/vapi-mcp-server.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "dist/vapi-mcp-server.js" + } + ] + } + ``` + +## 🎯 **Step 2: Configure Vapi Dashboard** + +### Create MCP Tool + +1. **Go to Vapi Dashboard** → **Tools** +2. **Click "Create Tool"** +3. **Select "MCP" from options** +4. **Configure the tool:** + +#### **Tool Configuration:** + +```json +{ + "type": "mcp", + "function": { + "name": "ghlMcpTools" + }, + "server": { + "url": "https://your-app.up.railway.app" + } +} +``` + +**Important Notes:** +- ✅ **Use HTTP URL** (not HTTPS if testing locally) +- ✅ **No `/mcp` suffix** in serverUrl (Vapi handles routing) +- ✅ **Default protocol** is "shttp" (Streamable HTTP) - perfect! + +### Advanced Configuration (Optional) + +If you need custom headers or SSE fallback: + +```json +{ + "type": "mcp", + "function": { + "name": "ghlMcpTools" + }, + "server": { + "url": "https://your-app.up.railway.app", + "headers": { + "Authorization": "Bearer your-token", + "X-Custom-Header": "your-value" + } + }, + "metadata": { + "protocol": "shttp" + } +} +``` + +## 🎤 **Step 3: Add to Assistant** + +1. **Go to Vapi Dashboard** → **Assistants** +2. **Select your assistant** +3. **Go to Tools tab** +4. **Add your MCP tool from dropdown** +5. **Click "Publish"** + +### Sample Assistant Configuration + +```json +{ + "model": { + "provider": "openai", + "model": "gpt-4o", + "messages": [ + { + "role": "system", + "content": "You are a helpful sales assistant with access to GoHighLevel CRM tools. You can help with contacts, appointments, conversations, and more.\n\nAvailable tools include:\n- Contact management (create, search, update contacts)\n- Calendar booking (get free slots, create appointments)\n- Verification (email, SMS, WhatsApp)\n- Conversations (send SMS/email)\n- And 200+ more GoHighLevel tools\n\nAlways be professional and helpful when using these tools." + } + ], + "tools": [ + { + "type": "mcp", + "function": { + "name": "ghlMcpTools" + }, + "server": { + "url": "https://your-app.up.railway.app" + } + } + ] + } +} +``` + +## 🔍 **Step 4: Test the Integration** + +### 1. Health Check +```bash +curl https://your-app.up.railway.app/health +``` + +**Expected Response:** +```json +{ + "status": "healthy", + "server": "vapi-ghl-mcp-server", + "protocol": "streamable-http", + "tools": { + "total": 221 + }, + "ghl": { + "connected": true, + "locationId": "your_location_id" + } +} +``` + +### 2. Tools List Test +```bash +curl -X POST https://your-app.up.railway.app/mcp/tools/list \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' +``` + +### 3. Test with Vapi Assistant +Try these commands with your assistant: +- "Search for a contact named John" +- "Create a new contact with email test@example.com" +- "Get available appointment slots for tomorrow" + +## 🛠️ **Troubleshooting** + +### Common Issues + +#### ❌ **500 Error - Protocol Mismatch** +**Problem:** Using SSE server with Vapi +**Solution:** Use the new `vapi-mcp-server.ts` (this guide) + +#### ❌ **No Tools Found** +**Problem:** Server not responding to tool list requests +**Solution:** Check `/mcp/tools/list` endpoint directly + +#### ❌ **Authentication Failed** +**Problem:** Invalid GHL API credentials +**Solution:** Verify `GHL_API_KEY` and `GHL_LOCATION_ID` + +### Debug Endpoints + +- **Health:** `/health` +- **Server Info:** `/` +- **Tools List:** `/mcp/tools/list` +- **Debug:** `/debug` + +### Environment Variables + +```env +# Required +GHL_API_KEY=your_api_key +GHL_LOCATION_ID=your_location_id + +# Optional +GHL_BASE_URL=https://services.leadconnectorhq.com +VAPI_MCP_PORT=8001 +PORT=8001 +``` + +## 📚 **Available Tools (221 Total)** + +The MCP server provides access to all GoHighLevel tools: + +### 🏷️ **Core Tools:** +- **Contacts:** Create, search, update, verify (email/SMS/WhatsApp) +- **Calendar:** Get slots, book appointments, manage calendars +- **Conversations:** Send SMS/email, manage conversations +- **Opportunities:** CRM pipeline management +- **Workflows:** Trigger and manage automation + +### 📈 **Advanced Tools:** +- **Social Media:** Post management and scheduling +- **E-commerce:** Products, orders, inventory +- **Surveys:** Form submissions and responses +- **Media:** File upload and management +- **Custom Objects:** Custom data structures + +## 🔄 **Local Development** + +For local testing: + +```bash +# Install dependencies +npm install + +# Start Vapi server in development +npm run dev:vapi + +# Test locally +curl http://localhost:8001/health +``` + +## 🎯 **Next Steps** + +1. ✅ **Deploy server** and get your URL +2. ✅ **Configure Vapi MCP tool** with serverUrl +3. ✅ **Add to assistant** and publish +4. ✅ **Test basic functionality** +5. 🚀 **Build your voice workflows!** + +## 📞 **Support** + +- **Server Issues:** Check `/health` and `/debug` endpoints +- **Tool Errors:** Review server logs for specific error messages +- **Vapi Integration:** Refer to [Vapi MCP Documentation](https://docs.vapi.ai/mcp) + +**🎉 Your GoHighLevel tools are now available in Vapi AI!** diff --git a/package-lock.json b/package-lock.json index f733e19b..31aad0cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,36 @@ { - "name": "ghl-mcp", + "name": "@mastanley13/ghl-mcp-server", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ghl-mcp", + "name": "@mastanley13/ghl-mcp-server", "version": "1.0.0", "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.18.1", "@types/cors": "^2.8.18", "@types/express": "^5.0.2", + "@types/node": "^24.5.2", "axios": "^1.9.0", "cors": "^2.8.5", "dotenv": "^16.5.0", - "express": "^5.1.0" + "express": "^5.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + }, + "bin": { + "ghl-mcp-server": "dist/http-server.js" }, "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "^22.15.29", + "@types/jest": "^30.0.0", "jest": "^29.7.0", "nodemon": "^3.1.10", - "ts-jest": "^29.3.4", - "ts-node": "^10.9.2", - "typescript": "^5.8.3" + "ts-jest": "^29.3.4" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@ampproject/remapping": { @@ -496,7 +502,6 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -508,7 +513,6 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -603,6 +607,16 @@ } } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -660,6 +674,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/globals": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", @@ -675,6 +699,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -835,7 +883,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -852,8 +899,7 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -866,15 +912,17 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.1.tgz", - "integrity": "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.1.tgz", + "integrity": "sha512-d//GE8/Yh7aC3e7p+kZG8JqqEAwwDUmAfvH1quogtbk+ksS6E0RR6toKKESPYYZVre0meqkJb27zb+dhqE9Sgw==", + "license": "MIT", "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", @@ -913,26 +961,22 @@ "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -1060,13 +1104,227 @@ } }, "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", "dev": true, + "license": "MIT", "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/expect-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", + "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/jest/node_modules/expect": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-diff": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-matcher-utils": { + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", + "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-message-util": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@types/mime": { @@ -1075,11 +1333,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { - "version": "22.15.29", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", - "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.12.0" } }, "node_modules/@types/qs": { @@ -1148,7 +1407,6 @@ "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -1160,7 +1418,6 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, "dependencies": { "acorn": "^8.11.0" }, @@ -1238,8 +1495,7 @@ "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" }, "node_modules/argparse": { "version": "1.0.10", @@ -1784,8 +2040,7 @@ "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -1868,7 +2123,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, "engines": { "node": ">=0.3.1" } @@ -3539,8 +3793,7 @@ "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" }, "node_modules/makeerror": { "version": "1.0.12", @@ -4650,7 +4903,6 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -4727,7 +4979,6 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4743,9 +4994,10 @@ "dev": true }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", @@ -4796,8 +5048,7 @@ "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" }, "node_modules/v8-to-istanbul": { "version": "9.3.0", @@ -4925,7 +5176,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, "engines": { "node": ">=6" } diff --git a/package.json b/package.json index b7e04be9..1bf1e1b1 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "@mastanley13/ghl-mcp-server", "version": "1.0.0", "description": "GoHighLevel MCP Server for Claude Desktop and ChatGPT integration", - "main": "dist/server.js", + "main": "dist/http-server.js", "bin": { - "ghl-mcp-server": "dist/server.js" + "ghl-mcp-server": "dist/http-server.js" }, "files": [ "dist/", @@ -17,15 +17,15 @@ "scripts": { "build": "tsc", "dev": "nodemon --exec ts-node src/http-server.ts", + "dev:vapi": "nodemon --exec ts-node src/vapi-mcp-server.ts", "start": "node dist/http-server.js", "start:stdio": "node dist/server.js", "start:http": "node dist/http-server.js", + "start:vapi": "node dist/vapi-mcp-server.js", "vercel-build": "npm run build", "prepublishOnly": "npm run build", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "lint": "tsc --noEmit" + "start:sse": "node dist/sse-simple.js", + "start:elevenlabs": "node dist/http-server.js" }, "keywords": [ "mcp", @@ -35,22 +35,22 @@ ], "author": "", "license": "ISC", - "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "^22.15.29", - "jest": "^29.7.0", - "nodemon": "^3.1.10", - "ts-jest": "^29.3.4", - "ts-node": "^10.9.2", - "typescript": "^5.8.3" - }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.18.1", "@types/cors": "^2.8.18", "@types/express": "^5.0.2", "axios": "^1.9.0", "cors": "^2.8.5", "dotenv": "^16.5.0", - "express": "^5.1.0" + "express": "^5.1.0", + "ts-node": "^10.9.2", + "typescript": "^5.8.3", + "@types/node": "^24.5.2" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "jest": "^29.7.0", + "nodemon": "^3.1.10", + "ts-jest": "^29.3.4" } } diff --git a/railway.json b/railway.json index 9e756a98..eb7cd353 100644 --- a/railway.json +++ b/railway.json @@ -4,7 +4,7 @@ "builder": "NIXPACKS" }, "deploy": { - "startCommand": "npm start", + "startCommand": "npm run start:vapi", "restartPolicyType": "ON_FAILURE", "restartPolicyMaxRetries": 10 } diff --git a/src/clients/ghl-api-client.ts b/src/clients/ghl-api-client.ts index 55603516..a6981926 100644 --- a/src/clients/ghl-api-client.ts +++ b/src/clients/ghl-api-client.ts @@ -484,13 +484,23 @@ export class GHLApiClient { locationId: contactData.locationId || this.config.locationId }; + console.log('[GHL API] Creating contact with payload:', JSON.stringify(payload, null, 2)); + console.log('[GHL API] Using location ID:', payload.locationId); + console.log('[GHL API] API endpoint:', `${this.config.baseUrl}/contacts/`); + const response: AxiosResponse<{ contact: GHLContact }> = await this.axiosInstance.post( '/contacts/', payload ); + console.log('[GHL API] Contact created successfully:', JSON.stringify(response.data, null, 2)); return this.wrapResponse(response.data.contact); } catch (error) { + console.error('[GHL API] Failed to create contact:', error); + if (axios.isAxiosError(error)) { + console.error('[GHL API] Error response:', error.response?.data); + console.error('[GHL API] Error status:', error.response?.status); + } throw error; } } @@ -608,7 +618,14 @@ export class GHLApiClient { payload ); - return this.wrapResponse(response.data); + // Ensure the response has the expected structure + const responseData = response.data || {}; + const validResponse: GHLSearchContactsResponse = { + contacts: Array.isArray(responseData.contacts) ? responseData.contacts : [], + total: responseData.total || 0 + }; + + return this.wrapResponse(validResponse); } catch (error) { const axiosError = error as AxiosError; process.stderr.write(`[GHL API] Search contacts error: ${JSON.stringify({ @@ -6825,4 +6842,27 @@ export class GHLApiClient { throw error; } } + + /** + * Trigger workflow for contact + * POST /contacts/{contactId}/workflow/{workflowId} + */ + async triggerWorkflow(params: { workflowId: string; contactId: string }): Promise> { + try { + console.log('[GHL API] Triggering workflow:', params); + + const response = await this.axiosInstance.post( + `/contacts/${params.contactId}/workflow/${params.workflowId}` + ); + + console.log('[GHL API] Workflow triggered successfully'); + return this.wrapResponse(response.data); + } catch (error) { + console.error('[GHL API] Failed to trigger workflow:', error); + if (axios.isAxiosError(error)) { + console.error('[GHL API] Workflow error response:', error.response?.data); + } + throw error; + } + } } \ No newline at end of file diff --git a/src/http-server.ts b/src/http-server.ts index 70882401..2d90b0c7 100644 --- a/src/http-server.ts +++ b/src/http-server.ts @@ -1,6 +1,6 @@ /** * GoHighLevel MCP HTTP Server - * HTTP version for ChatGPT web integration + * HTTP version for ChatGPT and ElevenLabs web integration */ import express from 'express'; @@ -115,16 +115,16 @@ class GHLMCPHttpServer { * Setup Express middleware and configuration */ private setupExpress(): void { - // Enable CORS for ChatGPT integration + // Enable CORS for ChatGPT and ElevenLabs integration this.app.use(cors({ - origin: ['https://chatgpt.com', 'https://chat.openai.com', 'http://localhost:*'], + origin: '*', methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'Accept'], - credentials: true + credentials: false })); - // Parse JSON requests - this.app.use(express.json()); + // JSON parsing for webhook routes only + this.app.use('/webhook', express.json()); // Request logging this.app.use((req, res, next) => { @@ -297,14 +297,39 @@ class GHLMCPHttpServer { */ private setupRoutes(): void { // Health check endpoint - this.app.get('/health', (req, res) => { - res.json({ - status: 'healthy', - server: 'ghl-mcp-server', - version: '1.0.0', - timestamp: new Date().toISOString(), - tools: this.getToolsCount() - }); + this.app.get('/health', async (req, res) => { + try { + // Test GHL API connection + const testResponse = await this.ghlClient.getLocationById(this.ghlClient.getConfig().locationId); + + res.json({ + status: 'healthy', + server: 'ghl-mcp-server', + version: '1.0.0', + timestamp: new Date().toISOString(), + tools: this.getToolsCount(), + ghl: { + connected: testResponse.success, + locationId: this.ghlClient.getConfig().locationId, + locationName: testResponse.data?.location?.name || 'Unknown', + baseUrl: this.ghlClient.getConfig().baseUrl + } + }); + } catch (error) { + res.json({ + status: 'unhealthy', + server: 'ghl-mcp-server', + version: '1.0.0', + timestamp: new Date().toISOString(), + tools: this.getToolsCount(), + ghl: { + connected: false, + error: error instanceof Error ? error.message : 'Unknown error', + locationId: this.ghlClient.getConfig().locationId, + baseUrl: this.ghlClient.getConfig().baseUrl + } + }); + } }); // MCP capabilities endpoint @@ -350,27 +375,31 @@ class GHLMCPHttpServer { } }); - // SSE endpoint for ChatGPT MCP connection + // SSE endpoint for MCP connection (works for both ChatGPT and ElevenLabs) const handleSSE = async (req: express.Request, res: express.Response) => { const sessionId = req.query.sessionId || 'unknown'; - console.log(`[GHL MCP HTTP] New SSE connection from: ${req.ip}, sessionId: ${sessionId}, method: ${req.method}`); + const isElevenLabs = req.url?.includes('/elevenlabs') || req.headers['user-agent']?.includes('python-httpx'); + const client = isElevenLabs ? 'ElevenLabs' : 'Claude/ChatGPT'; + console.log(`[${client} MCP] New SSE connection from: ${req.ip}, sessionId: ${sessionId}, method: ${req.method}, url: ${req.url}`); try { - // Create SSE transport (this will set the headers) + // Create SSE transport - always use '/sse' as the path for consistency const transport = new SSEServerTransport('/sse', res); // Connect MCP server to transport await this.server.connect(transport); - console.log(`[GHL MCP HTTP] SSE connection established for session: ${sessionId}`); + console.log(`[${client} MCP] SSE connection established for session: ${sessionId}`); + console.log(`[${client} MCP] Available tools: ${this.getToolsCount().total}`); // Handle client disconnect req.on('close', () => { - console.log(`[GHL MCP HTTP] SSE connection closed for session: ${sessionId}`); + console.log(`[${client} MCP] SSE connection closed for session: ${sessionId}`); }); } catch (error) { - console.error(`[GHL MCP HTTP] SSE connection error for session ${sessionId}:`, error); + console.error(`[${client} MCP] SSE connection error for session ${sessionId}:`, error); + console.error(`[${client} MCP] Error details:`, error instanceof Error ? error.stack : error); // Only send error response if headers haven't been sent yet if (!res.headersSent) { @@ -382,9 +411,547 @@ class GHLMCPHttpServer { } }; - // Handle both GET and POST for SSE (MCP protocol requirements) - this.app.get('/sse', handleSSE); - this.app.post('/sse', handleSSE); + // Enhanced debug logging for MCP messages + const logMCPMessage = (direction: string, client: string, message: any, sessionId: string) => { + console.log(`[${client} MCP ${direction}] Session: ${sessionId}`); + console.log(`[${client} MCP ${direction}] Message:`, JSON.stringify(message, null, 2)); + }; + + // Enhanced SSE handler with detailed MCP logging + const handleSSEWithLogging = async (req: express.Request, res: express.Response) => { + const sessionId = req.query.sessionId || 'unknown'; + const isElevenLabs = req.url?.includes('/elevenlabs') || req.headers['user-agent']?.includes('python-httpx'); + const client = isElevenLabs ? 'ElevenLabs' : 'Claude/ChatGPT'; + + console.log(`[${client} MCP] New SSE connection from: ${req.ip}, sessionId: ${sessionId}, method: ${req.method}, url: ${req.url}`); + + try { + // IMMEDIATELY create and connect the transport so it can handle the request + // The SSEServerTransport needs to set up its own event handlers on the request + const transport = new SSEServerTransport('/sse', res); + + // Add message interceptors for detailed logging BEFORE connecting + const originalSend = transport.send.bind(transport); + transport.send = (message: any) => { + console.log(`[${client} MCP SEND] Message:`, JSON.stringify(message, null, 2)); + return originalSend(message); + }; + + // Connect MCP server to transport IMMEDIATELY + // This allows the transport to handle the POST body + await this.server.connect(transport); + + console.log(`[${client} MCP] SSE connection established for session: ${sessionId}`); + console.log(`[${client} MCP] Available tools: ${this.getToolsCount().total}`); + + // Handle client disconnect + req.on('close', () => { + console.log(`[${client} MCP] SSE connection closed for session: ${sessionId}`); + }); + + } catch (error) { + console.error(`[${client} MCP] SSE connection error for session ${sessionId}:`, error); + console.error(`[${client} MCP] Error details:`, error instanceof Error ? error.stack : error); + + if (!res.headersSent) { + res.status(500).json({ error: 'Failed to establish SSE connection' }); + } else { + res.end(); + } + } + }; + + // Store active MCP transports - use both session ID and a simple counter for fallback + const activeTransports = new Map(); + const transportsByIndex = new Map(); + let transportIndex = 0; + + // Handle GET for SSE connection establishment + this.app.get('/sse', async (req, res) => { + const sessionId = req.query.sessionId || 'unknown'; + const isElevenLabs = req.headers['user-agent']?.includes('python-httpx'); + const client = isElevenLabs ? 'ElevenLabs' : 'Claude/ChatGPT'; + const currentIndex = transportIndex++; + + console.log(`[${client} MCP] Establishing SSE connection #${currentIndex} for session: ${sessionId}`); + + // Create SSE transport and connect MCP server + const transport = new SSEServerTransport('/sse', res); + + // Add message interceptors for logging + const originalSend = transport.send.bind(transport); + transport.send = (message: any) => { + // Log concisely for tools/list responses to avoid rate limits + if (message.result?.tools && Array.isArray(message.result.tools)) { + console.log(`[${client} MCP SEND] Session: ${sessionId}`); + console.log(`[${client} MCP SEND] tools/list response with ${message.result.tools.length} tools`); + // Log just tool names, not full schemas + const toolNames = message.result.tools.map((t: any) => t.name).slice(0, 10); + console.log(`[${client} MCP SEND] First 10 tools:`, toolNames.join(', ')); + } else { + console.log(`[${client} MCP SEND] Session: ${sessionId}`); + console.log(`[${client} MCP SEND] Message:`, JSON.stringify(message, null, 2)); + } + return originalSend(message); + }; + + // Connect MCP server to transport + await this.server.connect(transport); + + // Store the transport multiple ways for robust lookup + activeTransports.set(sessionId.toString(), transport); + transportsByIndex.set(currentIndex, transport); + // Also store by IP for ElevenLabs (they might use same IP for GET/POST) + if (req.ip) { + activeTransports.set(`ip:${req.ip}`, transport); + } + + console.log(`[${client} MCP] SSE connection established for session: ${sessionId}, index: ${currentIndex}`); + console.log(`[${client} MCP] Available tools: ${this.getToolsCount().total}`); + console.log(`[${client} MCP] Active transports: ${activeTransports.size}`); + + // Clean up on disconnect + req.on('close', () => { + console.log(`[${client} MCP] SSE connection closed for session: ${sessionId}`); + activeTransports.delete(sessionId.toString()); + transportsByIndex.delete(currentIndex); + if (req.ip) { + activeTransports.delete(`ip:${req.ip}`); + } + }); + }); + + // Handle POST for MCP messages + this.app.post('/sse', express.json(), async (req, res) => { + const sessionId = req.query.sessionId || 'unknown'; + const isElevenLabs = req.headers['user-agent']?.includes('python-httpx'); + const client = isElevenLabs ? 'ElevenLabs' : 'Claude/ChatGPT'; + + console.log(`[${client} MCP] POST message received for session: ${sessionId}`); + // Log request concisely + if (req.body?.method === 'tools/list') { + console.log(`[${client} MCP] POST body: tools/list request`); + } else { + console.log(`[${client} MCP] POST body:`, JSON.stringify(req.body, null, 2)); + } + + if (req.body) { + // Skip detailed logging for tools/list to avoid rate limits + if (req.body?.method !== 'tools/list') { + logMCPMessage('RECV', client, req.body, sessionId.toString()); + } + + // Try to find the transport - ElevenLabs might not send matching session IDs + let transport = activeTransports.get(sessionId.toString()); + + // If not found by session ID, try by IP + if (!transport && req.ip) { + transport = activeTransports.get(`ip:${req.ip}`); + if (transport) { + console.log(`[${client} MCP] Found transport by IP: ${req.ip}`); + } + } + + // If still not found, use the most recent transport (last resort) + if (!transport && transportsByIndex.size > 0) { + const lastIndex = Math.max(...Array.from(transportsByIndex.keys())); + transport = transportsByIndex.get(lastIndex); + if (transport) { + console.log(`[${client} MCP] Using most recent transport (index: ${lastIndex})`); + } + } + + if (transport) { + // The transport should handle the message internally + // Since we can't directly send to the server, we'll send a manual response for now + if (req.body.method === 'initialize') { + // Use the client's requested protocol version if we support it + const clientVersion = req.body.params?.protocolVersion || '2024-11-05'; + const supportedVersions = ['2024-11-05', '2025-03-26']; + const protocolVersion = supportedVersions.includes(clientVersion) ? clientVersion : '2024-11-05'; + + const response = { + jsonrpc: '2.0', + id: req.body.id, + result: { + protocolVersion: protocolVersion, + capabilities: { + tools: {} + }, + serverInfo: { + name: 'ghl-mcp-server', + version: '1.0.0' + } + } + }; + // Send response through SSE + transport.send(response); + console.log(`[${client} MCP] Sent initialize response with protocol version: ${protocolVersion}`); + } else if (req.body.method === 'tools/list') { + const tools = this.getAllToolDefinitions(); + const response = { + jsonrpc: '2.0', + id: req.body.id, + result: { + tools: tools + } + }; + transport.send(response); + console.log(`[${client} MCP] Sent tools/list response with ${tools.length} tools`); + } else if (req.body.method === 'tools/call') { + // Handle tool execution + const { name, arguments: args } = req.body.params || {}; + console.log(`[${client} MCP] Tool call requested: ${name}`); + console.log(`[${client} MCP] Tool arguments:`, JSON.stringify(args, null, 2)); + + try { + // Execute the tool using the existing handlers + const result = await this.executeToolCall(name, args); + console.log(`[${client} MCP] Tool execution result:`, JSON.stringify(result, null, 2)); + const response = { + jsonrpc: '2.0', + id: req.body.id, + result: { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + } + }; + transport.send(response); + console.log(`[${client} MCP] Tool call successful: ${name}`); + } catch (error) { + const errorResponse = { + jsonrpc: '2.0', + id: req.body.id, + error: { + code: -32603, + message: error instanceof Error ? error.message : 'Tool execution failed' + } + }; + transport.send(errorResponse); + console.error(`[${client} MCP] Tool call failed: ${name}`, error); + } + } + } else { + console.error(`[${client} MCP] No transport found for session: ${sessionId}`); + } + + // Acknowledge the POST request + res.status(200).json({ status: 'received' }); + } else { + res.status(400).json({ error: 'No body received' }); + } + }); + + // ElevenLabs MCP endpoint - Same pattern as /sse + this.app.get('/elevenlabs', async (req, res) => { + const sessionId = req.query.sessionId || 'unknown'; + const client = 'ElevenLabs'; + const currentIndex = transportIndex++; + + console.log(`[${client} MCP] Establishing SSE connection #${currentIndex} for session: ${sessionId}`); + + // Create SSE transport and connect MCP server + const transport = new SSEServerTransport('/sse', res); + + // Add message interceptors for logging + const originalSend = transport.send.bind(transport); + transport.send = (message: any) => { + // Log concisely for tools/list responses to avoid rate limits + if (message.result?.tools && Array.isArray(message.result.tools)) { + console.log(`[${client} MCP SEND] Session: ${sessionId}`); + console.log(`[${client} MCP SEND] tools/list response with ${message.result.tools.length} tools`); + // Log just tool names, not full schemas + const toolNames = message.result.tools.map((t: any) => t.name).slice(0, 10); + console.log(`[${client} MCP SEND] First 10 tools:`, toolNames.join(', ')); + } else { + console.log(`[${client} MCP SEND] Session: ${sessionId}`); + console.log(`[${client} MCP SEND] Message:`, JSON.stringify(message, null, 2)); + } + return originalSend(message); + }; + + // Connect MCP server to transport + await this.server.connect(transport); + + // Store the transport multiple ways for robust lookup + activeTransports.set(sessionId.toString(), transport); + transportsByIndex.set(currentIndex, transport); + if (req.ip) { + activeTransports.set(`ip:${req.ip}`, transport); + } + + console.log(`[${client} MCP] SSE connection established for session: ${sessionId}, index: ${currentIndex}`); + console.log(`[${client} MCP] Available tools: ${this.getToolsCount().total}`); + console.log(`[${client} MCP] Active transports: ${activeTransports.size}`); + + // Clean up on disconnect + req.on('close', () => { + console.log(`[${client} MCP] SSE connection closed for session: ${sessionId}`); + activeTransports.delete(sessionId.toString()); + transportsByIndex.delete(currentIndex); + if (req.ip) { + activeTransports.delete(`ip:${req.ip}`); + } + }); + }); + + this.app.post('/elevenlabs', express.json(), async (req, res) => { + const sessionId = req.query.sessionId || 'unknown'; + const client = 'ElevenLabs'; + + console.log(`[${client} MCP] POST message received for session: ${sessionId}`); + // Log request concisely + if (req.body?.method === 'tools/list') { + console.log(`[${client} MCP] POST body: tools/list request`); + } else { + console.log(`[${client} MCP] POST body:`, JSON.stringify(req.body, null, 2)); + } + + if (req.body) { + // Skip detailed logging for tools/list to avoid rate limits + if (req.body?.method !== 'tools/list') { + logMCPMessage('RECV', client, req.body, sessionId.toString()); + } + + // Try to find the transport - ElevenLabs might not send matching session IDs + let transport = activeTransports.get(sessionId.toString()); + + // If not found by session ID, try by IP + if (!transport && req.ip) { + transport = activeTransports.get(`ip:${req.ip}`); + if (transport) { + console.log(`[${client} MCP] Found transport by IP: ${req.ip}`); + } + } + + // If still not found, use the most recent transport (last resort) + if (!transport && transportsByIndex.size > 0) { + const lastIndex = Math.max(...Array.from(transportsByIndex.keys())); + transport = transportsByIndex.get(lastIndex); + if (transport) { + console.log(`[${client} MCP] Using most recent transport (index: ${lastIndex})`); + } + } + + if (transport) { + if (req.body.method === 'initialize') { + // Use the client's requested protocol version if we support it + const clientVersion = req.body.params?.protocolVersion || '2024-11-05'; + const supportedVersions = ['2024-11-05', '2025-03-26']; + const protocolVersion = supportedVersions.includes(clientVersion) ? clientVersion : '2024-11-05'; + + const response = { + jsonrpc: '2.0', + id: req.body.id, + result: { + protocolVersion: protocolVersion, + capabilities: { + tools: {} + }, + serverInfo: { + name: 'ghl-mcp-server', + version: '1.0.0' + } + } + }; + transport.send(response); + console.log(`[${client} MCP] Sent initialize response with protocol version: ${protocolVersion}`); + } else if (req.body.method === 'tools/list') { + const tools = this.getAllToolDefinitions(); + const response = { + jsonrpc: '2.0', + id: req.body.id, + result: { + tools: tools + } + }; + transport.send(response); + console.log(`[${client} MCP] Sent tools/list response with ${tools.length} tools`); + } else if (req.body.method === 'tools/call') { + // Handle tool execution + const { name, arguments: args } = req.body.params || {}; + console.log(`[${client} MCP] Tool call requested: ${name}`); + console.log(`[${client} MCP] Tool arguments:`, JSON.stringify(args, null, 2)); + + try { + // Execute the tool using the existing handlers + const result = await this.executeToolCall(name, args); + console.log(`[${client} MCP] Tool execution result:`, JSON.stringify(result, null, 2)); + const response = { + jsonrpc: '2.0', + id: req.body.id, + result: { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + } + }; + transport.send(response); + console.log(`[${client} MCP] Tool call successful: ${name}`); + } catch (error) { + const errorResponse = { + jsonrpc: '2.0', + id: req.body.id, + error: { + code: -32603, + message: error instanceof Error ? error.message : 'Tool execution failed' + } + }; + transport.send(errorResponse); + console.error(`[${client} MCP] Tool call failed: ${name}`, error); + } + } + } else { + console.error(`[${client} MCP] No transport found for session: ${sessionId}`); + } + + res.status(200).json({ status: 'received' }); + } else { + res.status(400).json({ error: 'No body received' }); + } + }); + + // Buffer test endpoint - try different approaches + this.app.post('/test-buffer', express.text({ type: 'application/json' }), (req, res) => { + console.log(`[Buffer Test] Body type:`, typeof req.body); + console.log(`[Buffer Test] Raw body:`, req.body); + console.log(`[Buffer Test] String body:`, String(req.body)); + + res.json({ + received: true, + type: typeof req.body, + content: String(req.body) + }); + }); + + // ElevenLabs debug endpoint to understand the protocol + this.app.all('/elevenlabs-debug', (req, res) => { + console.log(`[ElevenLabs Debug] ${req.method} request`); + console.log(`[ElevenLabs Debug] Headers:`, JSON.stringify(req.headers, null, 2)); + console.log(`[ElevenLabs Debug] Query:`, req.query); + console.log(`[ElevenLabs Debug] URL:`, req.url); + + if (req.method === 'POST') { + let body = ''; + req.on('data', (chunk) => { + body += chunk.toString(); + }); + req.on('end', () => { + console.log(`[ElevenLabs Debug] Body:`, body); + res.json({ status: 'debug', received: body }); + }); + } else { + // For GET requests, set up SSE + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*' + }); + + // Send a test message + res.write(`data: {"type": "debug", "message": "ElevenLabs debug endpoint connected"}\n\n`); + + // Log any incoming data + req.on('data', (chunk) => { + console.log(`[ElevenLabs Debug] Received data on GET:`, chunk.toString()); + }); + + req.on('close', () => { + console.log(`[ElevenLabs Debug] Connection closed`); + }); + } + }); + + // Webhook handler for GHL contact updates + this.app.post('/webhook/update-contact', async (req, res) => { + try { + const { contactId, customFields, tags } = req.body; + console.log(`[Contact Update Webhook] Updating contact ${contactId}`); + + const updateData: any = {}; + if (customFields) updateData.customFields = customFields; + if (tags) updateData.tags = tags; + + const result = await this.contactTools.executeTool('update_contact', { + contactId: contactId, + ...updateData + }); + + console.log(`[Contact Update Webhook] Success:`, result); + res.json({ success: true, result: result }); + } catch (error) { + console.error(`[Contact Update Webhook] Error:`, error); + res.status(500).json({ error: error instanceof Error ? error.message : 'Update failed' }); + } + }); + + // Webhook handlers for GHL verification workflow + this.app.post('/webhook/code-sent', async (req, res) => { + const { contactId, code, email, phone, method } = req.body; + const recipient = email || phone || 'unknown'; + console.log(`[Verification Webhook] Code sent via ${method} to ${recipient} for contact ${contactId}`); + res.json({ received: true, method: method }); + }); + + this.app.post('/webhook/verified', async (req, res) => { + const { contactId, email, phone, status, method } = req.body; + const recipient = email || phone || 'unknown'; + console.log(`[Verification Webhook] ${method} verified: ${recipient} (${contactId})`); + res.json({ + received: true, + message: 'Verification successful', + method: method + }); + }); + + this.app.post('/webhook/verification-failed', async (req, res) => { + const { contactId, email, phone, reason, method } = req.body; + const recipient = email || phone || 'unknown'; + console.log(`[Verification Webhook] ${method} verification failed for ${recipient}: ${reason}`); + res.json({ + received: true, + message: 'Verification failed', + method: method, + reason: reason + }); + }); + + // Debug endpoint for calendar testing + this.app.get('/debug/test-calendar/:calendarId', async (req, res) => { + try { + const calendarId = req.params.calendarId; + console.log(`[DEBUG] Testing calendar access: ${calendarId}`); + + // Use existing calendar tools to test access + // Test free slots functionality + const testResult = await this.calendarTools.executeTool('get_free_slots', { + calendarId: calendarId, + startDate: '2025-09-25', + endDate: '2025-09-25', + timezone: 'Asia/Singapore' + }); + + res.json({ + success: true, + calendarId: calendarId, + testResult: testResult + }); + } catch (error: any) { + console.error(`[DEBUG] Calendar test failed:`, error.message); + res.status(500).json({ + error: error.message, + details: error.stack + }); + } + }); // Root endpoint with server info this.app.get('/', (req, res) => { @@ -396,7 +963,8 @@ class GHLMCPHttpServer { health: '/health', capabilities: '/capabilities', tools: '/tools', - sse: '/sse' + sse: '/sse', + elevenlabs: '/elevenlabs' }, tools: this.getToolsCount(), documentation: 'https://github.com/your-repo/ghl-mcp-server' @@ -404,6 +972,176 @@ class GHLMCPHttpServer { }); } + /** + * Execute a tool call + */ + private async executeToolCall(name: string, args: any) { + // Route to appropriate tool handler based on tool name + if (this.isContactTool(name)) { + return await this.contactTools.executeTool(name, args || {}); + } else if (this.isConversationTool(name)) { + return await this.conversationTools.executeTool(name, args || {}); + } else if (this.isBlogTool(name)) { + return await this.blogTools.executeTool(name, args || {}); + } else if (this.isOpportunityTool(name)) { + return await this.opportunityTools.executeTool(name, args || {}); + } else if (this.isCalendarTool(name)) { + return await this.calendarTools.executeTool(name, args || {}); + } else if (this.isEmailTool(name)) { + return await this.emailTools.executeTool(name, args || {}); + } else if (this.isLocationTool(name)) { + return await this.locationTools.executeTool(name, args || {}); + } else if (this.isEmailISVTool(name)) { + return await this.emailISVTools.executeTool(name, args || {}); + } else if (this.isSocialMediaTool(name)) { + return await this.socialMediaTools.executeTool(name, args || {}); + } else if (this.isMediaTool(name)) { + return await this.mediaTools.executeTool(name, args || {}); + } else if (this.isObjectTool(name)) { + return await this.objectTools.executeTool(name, args || {}); + } else if (this.isAssociationTool(name)) { + return await this.associationTools.executeAssociationTool(name, args || {}); + } else if (this.isCustomFieldV2Tool(name)) { + return await this.customFieldV2Tools.executeCustomFieldV2Tool(name, args || {}); + } else if (this.isWorkflowTool(name)) { + return await this.workflowTools.executeWorkflowTool(name, args || {}); + } else if (this.isSurveyTool(name)) { + return await this.surveyTools.executeSurveyTool(name, args || {}); + } else if (this.isStoreTool(name)) { + return await this.storeTools.executeStoreTool(name, args || {}); + } else if (this.isProductsTool(name)) { + return await this.productsTools.executeProductsTool(name, args || {}); + } else { + throw new Error(`Unknown tool: ${name}`); + } + } + + /** + * Get all tool definitions for ElevenLabs + */ + private getAllToolDefinitions() { + const contactTools = this.contactTools.getToolDefinitions(); + const conversationTools = this.conversationTools.getToolDefinitions(); + const blogTools = this.blogTools.getToolDefinitions(); + const opportunityTools = this.opportunityTools.getToolDefinitions(); + const calendarTools = this.calendarTools.getToolDefinitions(); + const emailTools = this.emailTools.getToolDefinitions(); + const locationTools = this.locationTools.getToolDefinitions(); + const emailISVTools = this.emailISVTools.getToolDefinitions(); + const socialMediaTools = this.socialMediaTools.getTools(); + const mediaTools = this.mediaTools.getToolDefinitions(); + const objectTools = this.objectTools.getToolDefinitions(); + const associationTools = this.associationTools.getTools(); + const customFieldV2Tools = this.customFieldV2Tools.getTools(); + const workflowTools = this.workflowTools.getTools(); + const surveyTools = this.surveyTools.getTools(); + const storeTools = this.storeTools.getTools(); + const productsTools = this.productsTools.getTools(); + + return [ + ...contactTools, + ...conversationTools, + ...blogTools, + ...opportunityTools, + ...calendarTools, + ...emailTools, + ...locationTools, + ...emailISVTools, + ...socialMediaTools, + ...mediaTools, + ...objectTools, + ...associationTools, + ...customFieldV2Tools, + ...workflowTools, + ...surveyTools, + ...storeTools, + ...productsTools + ]; + } + + /** + * Handle tool call for ElevenLabs + */ + private async handleElevenLabsToolCall(message: any, res: express.Response) { + try { + const { name, arguments: args } = message.params; + console.log(`[ElevenLabs MCP] Executing tool: ${name}`); + + let result: any; + + // Route to appropriate tool handler (same logic as main server) + if (this.isContactTool(name)) { + result = await this.contactTools.executeTool(name, args || {}); + } else if (this.isConversationTool(name)) { + result = await this.conversationTools.executeTool(name, args || {}); + } else if (this.isBlogTool(name)) { + result = await this.blogTools.executeTool(name, args || {}); + } else if (this.isOpportunityTool(name)) { + result = await this.opportunityTools.executeTool(name, args || {}); + } else if (this.isCalendarTool(name)) { + result = await this.calendarTools.executeTool(name, args || {}); + } else if (this.isEmailTool(name)) { + result = await this.emailTools.executeTool(name, args || {}); + } else if (this.isLocationTool(name)) { + result = await this.locationTools.executeTool(name, args || {}); + } else if (this.isEmailISVTool(name)) { + result = await this.emailISVTools.executeTool(name, args || {}); + } else if (this.isSocialMediaTool(name)) { + result = await this.socialMediaTools.executeTool(name, args || {}); + } else if (this.isMediaTool(name)) { + result = await this.mediaTools.executeTool(name, args || {}); + } else if (this.isObjectTool(name)) { + result = await this.objectTools.executeTool(name, args || {}); + } else if (this.isAssociationTool(name)) { + result = await this.associationTools.executeAssociationTool(name, args || {}); + } else if (this.isCustomFieldV2Tool(name)) { + result = await this.customFieldV2Tools.executeCustomFieldV2Tool(name, args || {}); + } else if (this.isWorkflowTool(name)) { + result = await this.workflowTools.executeWorkflowTool(name, args || {}); + } else if (this.isSurveyTool(name)) { + result = await this.surveyTools.executeSurveyTool(name, args || {}); + } else if (this.isStoreTool(name)) { + result = await this.storeTools.executeStoreTool(name, args || {}); + } else if (this.isProductsTool(name)) { + result = await this.productsTools.executeProductsTool(name, args || {}); + } else { + throw new Error(`Unknown tool: ${name}`); + } + + // Send success response + const response = { + jsonrpc: '2.0', + id: message.id, + result: { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + } + }; + + res.write(`data: ${JSON.stringify(response)}\n\n`); + console.log(`[ElevenLabs MCP] Tool ${name} executed successfully`); + + } catch (error) { + console.error(`[ElevenLabs MCP] Error executing tool:`, error); + + // Send error response + const errorResponse = { + jsonrpc: '2.0', + id: message.id, + error: { + code: -32603, + message: `Tool execution failed: ${error instanceof Error ? error.message : String(error)}` + } + }; + + res.write(`data: ${JSON.stringify(errorResponse)}\n\n`); + } + } + /** * Get tools count summary */ @@ -454,6 +1192,9 @@ class GHLMCPHttpServer { // Basic Contact Management 'create_contact', 'search_contacts', 'get_contact', 'update_contact', 'add_contact_tags', 'remove_contact_tags', 'delete_contact', + // OTP/Verification Tools (Updated) + 'start_email_verification', 'start_sms_verification', 'start_whatsapp_verification', + 'verify_code', 'resend_verification_code', 'check_verification_status', // Task Management 'get_contact_tasks', 'create_contact_task', 'get_contact_task', 'update_contact_task', 'delete_contact_task', 'update_task_completion', @@ -695,8 +1436,9 @@ class GHLMCPHttpServer { console.log('✅ GoHighLevel MCP HTTP Server started successfully!'); console.log(`🌐 Server running on: http://0.0.0.0:${this.port}`); console.log(`🔗 SSE Endpoint: http://0.0.0.0:${this.port}/sse`); + console.log(`🔗 ElevenLabs Endpoint: http://0.0.0.0:${this.port}/elevenlabs`); console.log(`📋 Tools Available: ${this.getToolsCount().total}`); - console.log('🎯 Ready for ChatGPT integration!'); + console.log('🎯 Ready for ChatGPT and ElevenLabs integration!'); console.log('========================================='); }); @@ -742,4 +1484,4 @@ async function main(): Promise { main().catch((error) => { console.error('Unhandled error:', error); process.exit(1); -}); \ No newline at end of file +}); diff --git a/src/sse-simple.ts b/src/sse-simple.ts new file mode 100644 index 00000000..f42fd5aa --- /dev/null +++ b/src/sse-simple.ts @@ -0,0 +1,93 @@ +import express from 'express'; +import cors from 'cors'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(cors()); +app.use(express.json()); + +app.get('/sse-simple', (req, res) => { + console.log('[SSE] New connection from ElevenLabs'); + + res.writeHead(200, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*' + }); + + // Send tools in ElevenLabs format + const message = { + tools: [ + { + name: 'search_contacts', + description: 'Search for contacts by phone', + parameters: { + type: 'object', + properties: { + phone: { type: 'string', description: 'Phone number' } + } + } + }, + { + name: 'get_free_slots', + description: 'Get available appointment slots', + parameters: { + type: 'object', + properties: { + calendarId: { type: 'string', description: 'Calendar ID' }, + startDate: { type: 'string', description: 'Start date' }, + endDate: { type: 'string', description: 'End date' } + } + } + }, + { + name: 'create_appointment', + description: 'Book an appointment', + parameters: { + type: 'object', + properties: { + calendarId: { type: 'string', description: 'Calendar ID' }, + contactId: { type: 'string', description: 'Contact ID' }, + startTime: { type: 'string', description: 'Start time ISO format' } + } + } + } + ] + }; + + // Send as SSE event + res.write(`event: tools\n`); + res.write(`data: ${JSON.stringify(message)}\n\n`); + + // Keep alive + const interval = setInterval(() => { + res.write(': keepalive\n\n'); + }, 30000); + + req.on('close', () => { + console.log('[SSE] Connection closed'); + clearInterval(interval); + }); +}); + +// Tool execution endpoint +app.post('/execute', async (req, res) => { + const { tool, parameters } = req.body; + console.log('[Execute] Tool:', tool, 'Params:', parameters); + + // Add your GHL API calls here + res.json({ success: true, result: 'Tool executed' }); +}); + +app.get('/', (req, res) => { + res.json({ status: 'SSE Simple Server Running' }); +}); + +app.listen(PORT, () => { + console.log(`SSE Simple Server on port ${PORT}`); +}); diff --git a/src/tools/contact-tools.ts b/src/tools/contact-tools.ts index b51e6d62..764b7ad0 100644 --- a/src/tools/contact-tools.ts +++ b/src/tools/contact-tools.ts @@ -11,6 +11,7 @@ import { MCPUpdateContactParams, MCPAddContactTagsParams, MCPRemoveContactTagsParams, + GHLCustomField, // Task Management MCPGetContactTasksParams, MCPCreateContactTaskParams, @@ -85,15 +86,17 @@ export class ContactTools { }, { name: 'search_contacts', - description: 'Search for contacts with advanced filtering options', + description: 'Search for contacts by email, phone, or name. CRITICAL: Always use the "query" parameter - never use email or phone parameters as they cause API errors.', inputSchema: { type: 'object', properties: { - query: { type: 'string', description: 'Search query string' }, - email: { type: 'string', description: 'Filter by email address' }, - phone: { type: 'string', description: 'Filter by phone number' }, + query: { + type: 'string', + description: 'REQUIRED: Use this for email addresses, phone numbers, or names. Examples: "john@example.com", "+1234567890", "John Smith"' + }, limit: { type: 'number', description: 'Maximum number of results (default: 25)' } - } + }, + required: ['query'] } }, { @@ -109,7 +112,7 @@ export class ContactTools { }, { name: 'update_contact', - description: 'Update contact information', + description: 'Update contact information including custom fields', inputSchema: { type: 'object', properties: { @@ -118,7 +121,12 @@ export class ContactTools { lastName: { type: 'string', description: 'Contact last name' }, email: { type: 'string', description: 'Contact email address' }, phone: { type: 'string', description: 'Contact phone number' }, - tags: { type: 'array', items: { type: 'string' }, description: 'Tags to assign to contact' } + tags: { type: 'array', items: { type: 'string' }, description: 'Tags to assign to contact' }, + customFields: { + type: 'object', + description: 'Custom field updates as key-value pairs where key is field ID', + additionalProperties: { type: 'string' } + } }, required: ['contactId'] } @@ -477,6 +485,85 @@ export class ContactTools { }, required: ['contactId', 'workflowId'] } + }, + + // OTP/Verification Tools + { + name: 'start_email_verification', + description: 'Start email verification process by triggering GHL workflow', + inputSchema: { + type: 'object', + properties: { + email: { type: 'string', description: 'Email address to verify' }, + firstName: { type: 'string', description: 'User first name (optional)' }, + lastName: { type: 'string', description: 'User last name (optional)' } + }, + required: ['email'] + } + }, + { + name: 'start_sms_verification', + description: 'Start SMS verification process by adding sms-code tag', + inputSchema: { + type: 'object', + properties: { + phone: { type: 'string', description: 'Phone number to verify' }, + firstName: { type: 'string', description: 'User first name (optional)' }, + lastName: { type: 'string', description: 'User last name (optional)' }, + contactId: { type: 'string', description: 'Existing contact ID to update with phone (widget flow - optional)' } + }, + required: ['phone'] + } + }, + { + name: 'start_whatsapp_verification', + description: 'Start WhatsApp verification process by adding whatsapp-code tag', + inputSchema: { + type: 'object', + properties: { + phone: { type: 'string', description: 'Phone number to verify' }, + firstName: { type: 'string', description: 'User first name (optional)' }, + lastName: { type: 'string', description: 'User last name (optional)' }, + contactId: { type: 'string', description: 'Existing contact ID to update with phone (widget flow - optional)' } + }, + required: ['phone'] + } + }, + { + name: 'verify_code', + description: 'Verify the 6-digit code for email, SMS, or WhatsApp verification', + inputSchema: { + type: 'object', + properties: { + contactId: { type: 'string', description: 'Contact ID (from previous search_contacts call)' }, + code: { type: 'string', description: '6-digit verification code from user' }, + method: { type: 'string', enum: ['email', 'sms', 'whatsapp'], description: 'Verification method' } + }, + required: ['contactId', 'code', 'method'] + } + }, + { + name: 'resend_verification_code', + description: 'Resend verification code by clearing old code and restarting verification process', + inputSchema: { + type: 'object', + properties: { + contactId: { type: 'string', description: 'Contact ID (from previous search_contacts call)' }, + method: { type: 'string', enum: ['email', 'sms', 'whatsapp'], description: 'Verification method' } + }, + required: ['contactId', 'method'] + } + }, + { + name: 'check_verification_status', + description: 'Check if an email address has been verified recently', + inputSchema: { + type: 'object', + properties: { + email: { type: 'string', description: 'Email address to check' } + }, + required: ['email'] + } } ]; } @@ -564,6 +651,20 @@ export class ContactTools { return await this.addContactToWorkflow(params as MCPAddContactToWorkflowParams); case 'remove_contact_from_workflow': return await this.removeContactFromWorkflow(params as MCPRemoveContactFromWorkflowParams); + + // OTP/Verification Tools + case 'start_email_verification': + return await this.startEmailVerification(params); + case 'start_sms_verification': + return await this.startSmsVerification(params); + case 'start_whatsapp_verification': + return await this.startWhatsAppVerification(params); + case 'verify_code': + return await this.verifyCode(params); + case 'resend_verification_code': + return await this.resendVerificationCode(params); + case 'check_verification_status': + return await this.checkVerificationStatus(params); default: throw new Error(`Unknown tool: ${toolName}`); @@ -610,7 +711,16 @@ export class ContactTools { throw new Error(response.error?.message || 'Failed to search contacts'); } - return response.data!; + // Ensure we have a valid response structure + const data = response.data || { contacts: [], total: 0 }; + + // Additional safety check + if (!Array.isArray(data.contacts)) { + console.error('[ContactTools] Invalid response structure:', data); + return { contacts: [], total: 0 }; + } + + return data; } private async getContact(contactId: string): Promise { @@ -624,12 +734,22 @@ export class ContactTools { } private async updateContact(params: MCPUpdateContactParams): Promise { + // Prepare custom fields in GHL format if provided + let customFieldsArray: any[] | undefined = undefined; + if (params.customFields) { + customFieldsArray = Object.entries(params.customFields).map(([fieldId, value]) => ({ + id: fieldId, + value: value + })); + } + const response = await this.ghlClient.updateContact(params.contactId, { firstName: params.firstName, lastName: params.lastName, email: params.email, phone: params.phone, - tags: params.tags + tags: params.tags, + customFields: customFieldsArray }); if (!response.success) { @@ -969,4 +1089,624 @@ export class ContactTools { return response.data!; } + + // OTP/Verification Implementation + private verificationCodes = new Map(); + + /** + * Helper function to create proper GHL timezone format + */ + private getGHLTimestamp(): string { + const now = new Date(); + // Format as YYYY-MM-DDTHH:MM:SS+00:00 (GHL requires this exact format) + const year = now.getUTCFullYear(); + const month = String(now.getUTCMonth() + 1).padStart(2, '0'); + const day = String(now.getUTCDate()).padStart(2, '0'); + const hours = String(now.getUTCHours()).padStart(2, '0'); + const minutes = String(now.getUTCMinutes()).padStart(2, '0'); + const seconds = String(now.getUTCSeconds()).padStart(2, '0'); + + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}+00:00`; + } + + /** + * Comprehensive phone number formatting with country detection + * Returns: { formattedPhone: string, needsCountryConfirmation: boolean, detectedCountry?: string } + */ + private analyzePhoneNumber(phone: string): { + formattedPhone: string; + needsCountryConfirmation: boolean; + detectedCountry?: string; + suggestedFormat?: string; + } { + // Remove all non-digit characters except + + let cleaned = phone.replace(/[^\d+]/g, ''); + + // If already has + and looks complete, still need confirmation + if (cleaned.startsWith('+')) { + const countryCode = this.detectCountryFromPhone(cleaned); + return { + formattedPhone: cleaned, + needsCountryConfirmation: !countryCode, // Still confirm if country unclear + detectedCountry: countryCode, + suggestedFormat: cleaned + }; + } + + // Try to detect country from digits + const countryCode = this.detectCountryFromPhone(cleaned); + + if (countryCode) { + // Add + if country detected but missing + const formatted = cleaned.startsWith(countryCode.substring(1)) ? '+' + cleaned : countryCode + cleaned; + return { + formattedPhone: formatted, + needsCountryConfirmation: true, // Always confirm detected country + detectedCountry: countryCode, + suggestedFormat: formatted + }; + } + + // No country detected - definitely needs confirmation + return { + formattedPhone: '+1' + cleaned, // Default to US, but will ask for confirmation + needsCountryConfirmation: true, + suggestedFormat: '+1' + cleaned + }; + } + + /** + * Detect country from phone number patterns + */ + private detectCountryFromPhone(phone: string): string | undefined { + const digits = phone.replace(/[^\d]/g, ''); + + // Common country codes and patterns + if (digits.startsWith('1') && (digits.length === 11 || digits.length === 10)) { + return '+1'; // US/Canada + } + if (digits.startsWith('65') && digits.length === 10) { + return '+65'; // Singapore + } + if (digits.startsWith('60') && (digits.length === 11 || digits.length === 12)) { + return '+60'; // Malaysia + } + if (digits.startsWith('44') && digits.length === 12) { + return '+44'; // UK + } + if (digits.startsWith('86') && digits.length === 13) { + return '+86'; // China + } + if (digits.startsWith('91') && digits.length === 12) { + return '+91'; // India + } + if (digits.startsWith('61') && digits.length === 11) { + return '+61'; // Australia + } + if (digits.startsWith('33') && digits.length === 11) { + return '+33'; // France + } + if (digits.startsWith('49') && digits.length === 12) { + return '+49'; // Germany + } + + return undefined; // Unknown country + } + + /** + * Get country name from country code + */ + private getCountryName(countryCode: string): string { + const countries: { [key: string]: string } = { + '+1': 'United States/Canada', + '+65': 'Singapore', + '+60': 'Malaysia', + '+44': 'United Kingdom', + '+86': 'China', + '+91': 'India', + '+61': 'Australia', + '+33': 'France', + '+49': 'Germany' + }; + return countries[countryCode] || 'Unknown Country'; + } + + private async startEmailVerification(params: { email: string; firstName?: string; lastName?: string }) { + try { + // Check if contact exists, create if not + const contacts = await this.searchContacts({ query: params.email, limit: 1 }); + let contactId: string; + + if (contacts.contacts.length > 0) { + const foundContact = contacts.contacts[0]; + if (!foundContact.id) { + throw new Error('Contact found but missing ID'); + } + contactId = foundContact.id; + console.log('[OTP] Found existing contact:', contactId); + } else { + // Create new contact + const newContact = await this.createContact({ + email: params.email, + firstName: params.firstName || '', + lastName: params.lastName || '', + tags: ['verification-pending'] + }); + if (!newContact.id) { + throw new Error('Contact created but missing ID'); + } + contactId = newContact.id; + console.log('[OTP] Created new contact:', contactId); + } + + // Add email-code tag (this will trigger GHL workflow via tag conditions) + await this.addContactTags({ + contactId: contactId, + tags: ['email-code'] + }); + console.log('[Email Verification] Added email-code tag'); + + // Trigger verification workflow by adding contact to workflow + const workflowId = process.env.GHL_VERIFICATION_WORKFLOW_ID; + if (workflowId) { + try { + await this.addContactToWorkflow({ + contactId: contactId, + workflowId: workflowId, + eventStartTime: this.getGHLTimestamp() + }); + console.log('[OTP] Added contact to verification workflow:', workflowId); + } catch (workflowError) { + console.error('[OTP] Failed to add contact to workflow:', workflowError); + // Continue anyway - verification might still work with tags + } + } else { + console.warn('[OTP] GHL_VERIFICATION_WORKFLOW_ID not configured - emails may not send'); + } + + return { + success: true, + message: 'Verification code sent to your email', + contactId: contactId, + instructions: 'Please check your email and provide the 6-digit code' + }; + } catch (error) { + console.error('[OTP] Start verification error:', error); + throw new Error('Failed to start verification process'); + } + } + + /** + * Start SMS verification process by adding sms-code tag + */ + private async startSmsVerification(params: { phone: string; firstName?: string; lastName?: string; contactId?: string }) { + try { + // Format phone number with country code + const phoneAnalysis = this.analyzePhoneNumber(params.phone); + const formattedPhone = phoneAnalysis.formattedPhone; + console.log('[SMS Verification] Starting for:', params.phone, '→ formatted:', formattedPhone); + + // Note: In production, you should confirm country code with user + if (phoneAnalysis.needsCountryConfirmation) { + console.log('[SMS Verification] Country confirmation needed for:', params.phone, 'detected:', phoneAnalysis.detectedCountry); + } + + let contactId: string; + + // Widget flow: If contactId provided, update existing contact with phone + if (params.contactId) { + // Update existing contact with phone number (widget flow) + await this.updateContact({ + contactId: params.contactId, + phone: formattedPhone + }); + contactId = params.contactId; + console.log('[SMS Verification] Updated existing contact with phone:', contactId); + } else { + // Legacy flow: Search by phone, create if needed + const phoneSearch = await this.searchContacts({ query: formattedPhone, limit: 1 }); + + if (phoneSearch.contacts.length > 0) { + // Found contact by phone - use it + const foundContact = phoneSearch.contacts[0]; + if (!foundContact.id) { + throw new Error('Contact found but missing ID'); + } + contactId = foundContact.id; + console.log('[SMS Verification] Found existing contact by phone:', contactId); + } else { + // For SMS-only verification without existing contact, require minimal email + throw new Error('SMS verification requires either existing contactId or contact with email. For phone-only contacts, use start_email_verification first or provide contactId parameter.'); + } + } + + // Add sms-code tag (this will trigger GHL workflow via tag conditions) + await this.addContactTags({ + contactId: contactId, + tags: ['sms-code'] + }); + console.log('[SMS Verification] Added sms-code tag'); + + // Trigger verification workflow by adding contact to workflow + const workflowId = process.env.GHL_VERIFICATION_WORKFLOW_ID; + if (workflowId) { + try { + await this.addContactToWorkflow({ + contactId: contactId, + workflowId: workflowId, + eventStartTime: this.getGHLTimestamp() + }); + console.log('[SMS Verification] Added contact to verification workflow:', workflowId); + } catch (workflowError) { + console.error('[SMS Verification] Failed to add contact to workflow:', workflowError); + // Continue anyway - verification might still work with tags + } + } + + return { + success: true, + message: 'SMS verification started. Please check your phone for the verification code.', + contactId: contactId, + instructions: 'Please check your SMS and provide the 6-digit code within 5 minutes' + }; + } catch (error) { + console.error('[SMS Verification] Start verification error:', error); + throw new Error('Failed to start SMS verification process'); + } + } + + /** + * Start WhatsApp verification process by adding whatsapp-code tag + */ + private async startWhatsAppVerification(params: { phone: string; firstName?: string; lastName?: string; contactId?: string }) { + try { + // Format phone number with country code + const phoneAnalysis = this.analyzePhoneNumber(params.phone); + const formattedPhone = phoneAnalysis.formattedPhone; + console.log('[WhatsApp Verification] Starting for:', params.phone, '→ formatted:', formattedPhone); + + // Note: In production, you should confirm country code with user + if (phoneAnalysis.needsCountryConfirmation) { + console.log('[WhatsApp Verification] Country confirmation needed for:', params.phone, 'detected:', phoneAnalysis.detectedCountry); + } + + let contactId: string; + + // Widget flow: If contactId provided, update existing contact with phone + if (params.contactId) { + // Update existing contact with phone number (widget flow) + await this.updateContact({ + contactId: params.contactId, + phone: formattedPhone + }); + contactId = params.contactId; + console.log('[WhatsApp Verification] Updated existing contact with phone:', contactId); + } else { + // Legacy flow: Search by phone, create if needed + const phoneSearch = await this.searchContacts({ query: formattedPhone, limit: 1 }); + + if (phoneSearch.contacts.length > 0) { + // Found contact by phone - use it + const foundContact = phoneSearch.contacts[0]; + if (!foundContact.id) { + throw new Error('Contact found but missing ID'); + } + contactId = foundContact.id; + console.log('[WhatsApp Verification] Found existing contact by phone:', contactId); + } else { + // For WhatsApp-only verification without existing contact, require minimal email + throw new Error('WhatsApp verification requires either existing contactId or contact with email. For phone-only contacts, use start_email_verification first or provide contactId parameter.'); + } + } + + // Add whatsapp-code tag (this will trigger GHL workflow via tag conditions) + await this.addContactTags({ + contactId: contactId, + tags: ['whatsapp-code'] + }); + console.log('[WhatsApp Verification] Added whatsapp-code tag'); + + // Trigger verification workflow by adding contact to workflow + const workflowId = process.env.GHL_VERIFICATION_WORKFLOW_ID; + if (workflowId) { + try { + await this.addContactToWorkflow({ + contactId: contactId, + workflowId: workflowId, + eventStartTime: this.getGHLTimestamp() + }); + console.log('[WhatsApp Verification] Added contact to verification workflow:', workflowId); + } catch (workflowError) { + console.error('[WhatsApp Verification] Failed to add contact to workflow:', workflowError); + // Continue anyway - verification might still work with tags + } + } + + return { + success: true, + message: 'WhatsApp verification started. Please check WhatsApp for the verification code.', + contactId: contactId, + instructions: 'Please check your WhatsApp and provide the 6-digit code within 5 minutes' + }; + } catch (error) { + console.error('[WhatsApp Verification] Start verification error:', error); + throw new Error('Failed to start WhatsApp verification process'); + } + } + + /** + * Universal verification code checker - works for email, SMS, and WhatsApp + * FOOLPROOF: Uses contactId directly + getContact for full details + environment variable for field ID + */ + private async verifyCode(params: { contactId: string; code: string; method: 'email' | 'sms' | 'whatsapp' }) { + try { + console.log(`[Verify Code] Starting ${params.method} verification for contact:`, params.contactId); + + // Get FULL contact details using contactId (foolproof method) + const contactResponse = await this.ghlClient.getContact(params.contactId); + + if (!contactResponse.success || !contactResponse.data) { + console.error('[Verify Code] Failed to get contact details:', contactResponse.error); + return { + success: false, + message: 'Contact not found. Please start verification first.' + }; + } + + const contact = contactResponse.data; + console.log('[Verify Code] Got contact details, custom fields count:', contact.customFields?.length || 0); + + // Get verification code field ID from environment (primary) or fallback to name search + const verificationCodeFieldId = process.env.GHL_VERIFICATION_CODE_FIELD_ID; + let storedCode: string | undefined; + + if (verificationCodeFieldId) { + // Method 1: Use environment variable field ID (most reliable) + const fieldById = contact.customFields?.find(field => field.id === verificationCodeFieldId); + + // DEBUG: Log the entire field structure + console.log('[Verify Code] Found field structure:', JSON.stringify(fieldById, null, 2)); + + // GHL uses "value" property, not "field_value" + storedCode = (fieldById as any)?.value as string || + fieldById?.field_value as string || + (fieldById as any)?.fieldValue as string || + (fieldById as any)?.customValue as string; + + console.log('[Verify Code] Looking for field ID:', verificationCodeFieldId, 'Found:', !!fieldById); + console.log('[Verify Code] Raw field_value:', fieldById?.field_value); + console.log('[Verify Code] Raw value:', (fieldById as any)?.value); + console.log('[Verify Code] Extracted storedCode:', storedCode); + } + + if (!storedCode) { + // Method 2: Fallback - search by field name/key (backup method) + const fieldByName = contact.customFields?.find(field => + field.key === 'verification_code' || + field.id === 'verification_code' || + (field as any).name === 'verification_code' + ); + + storedCode = (fieldByName as any)?.value as string || + fieldByName?.field_value as string || + (fieldByName as any)?.fieldValue as string; + + console.log('[Verify Code] Fallback search found field:', !!fieldByName); + if (fieldByName) { + console.log('[Verify Code] Fallback field structure:', JSON.stringify(fieldByName, null, 2)); + } + } + + console.log('[Verify Code] Final stored code exists:', !!storedCode, 'User code:', params.code); + + if (!storedCode || storedCode.trim() === '') { + console.error('[Verify Code] No verification code found in custom fields'); + console.error('[Verify Code] Available custom fields with values:', contact.customFields?.map(f => ({ + id: f.id, + key: (f as any).key, + field_value: f.field_value, + value: (f as any).value, + rawField: JSON.stringify(f) + }))); + return { + success: false, + message: 'No verification code found. Please start verification process first.' + }; + } + + if (storedCode === params.code) { + // Determine the correct verified tag based on method + const verifiedTag = `verified-${params.method}`; + const pendingTag = `${params.method}-code`; + + console.log(`[Verify Code] Code matches! Adding ${verifiedTag} tag`); + + // Add verified tag - this will trigger the workflow to continue + await this.addContactTags({ + contactId: contact.id!, + tags: [verifiedTag] + }); + + // Remove pending verification tag + await this.removeContactTags({ + contactId: contact.id!, + tags: [pendingTag, 'verification-pending'] + }); + + // Clear the verification code field for security + const verificationFieldId = process.env.GHL_VERIFICATION_CODE_FIELD_ID || 'verification_code'; + const clearFieldUpdate: Record = {}; + clearFieldUpdate[verificationFieldId] = ''; + await this.updateContact({ + contactId: contact.id!, + customFields: clearFieldUpdate + }); + + return { + success: true, + message: `${params.method.charAt(0).toUpperCase() + params.method.slice(1)} verified successfully!`, + contactId: contact.id, + verifiedMethod: params.method + }; + } else { + console.log('[Verify Code] Code mismatch. Expected:', storedCode, 'Got:', params.code); + return { + success: false, + message: 'Invalid verification code. Please check and try again.', + expectedCode: storedCode, // For debugging only - remove in production + receivedCode: params.code + }; + } + } catch (error) { + console.error(`[Verify Code] ${params.method} verification error:`, error); + return { + success: false, + message: 'Verification failed. Please try again.', + error: error instanceof Error ? error.message : String(error) + }; + } + } + + /** + * Resend verification code - PROPER WORKFLOW RESET + * Step 1: Remove from workflow FIRST, then clear fields/tags, then restart + */ + private async resendVerificationCode(params: { contactId: string; method: 'email' | 'sms' | 'whatsapp' }) { + try { + console.log(`[Resend Verification] Starting ${params.method} resend for contact:`, params.contactId); + + // Get contact details to verify it exists + const contactResponse = await this.ghlClient.getContact(params.contactId); + + if (!contactResponse.success || !contactResponse.data) { + return { + success: false, + message: 'Contact not found' + }; + } + + const contact = contactResponse.data; + console.log('[Resend Verification] Contact found:', contact.email || contact.phone); + + // STEP 1: Remove from workflow FIRST (most important step) + const workflowId = process.env.GHL_VERIFICATION_WORKFLOW_ID; + if (workflowId) { + try { + await this.removeContactFromWorkflow({ + contactId: params.contactId, + workflowId: workflowId, + eventStartTime: this.getGHLTimestamp() + }); + console.log('[Resend Verification] ✅ Removed from workflow first'); + } catch (workflowError) { + console.error('[Resend Verification] ❌ Failed to remove from workflow:', workflowError); + // Don't continue if workflow removal fails - this could cause issues + return { + success: false, + message: 'Failed to reset verification workflow. Please try again.', + error: workflowError instanceof Error ? workflowError.message : String(workflowError) + }; + } + } + + // STEP 2: Clear verification code field + const verificationCodeFieldId = process.env.GHL_VERIFICATION_CODE_FIELD_ID || 'verification_code'; + const clearFieldUpdate: Record = {}; + clearFieldUpdate[verificationCodeFieldId] = ''; + + await this.updateContact({ + contactId: params.contactId, + customFields: clearFieldUpdate + }); + console.log('[Resend Verification] ✅ Cleared verification code field'); + + // STEP 3: Clear old verification tags + await this.removeContactTags({ + contactId: params.contactId, + tags: ['email-code', 'sms-code', 'whatsapp-code', 'verified-email', 'verified-sms', 'verified-whatsapp', 'verification-pending'] + }); + console.log('[Resend Verification] ✅ Cleared verification tags'); + + // STEP 4: Wait for cleanup to complete + await new Promise(resolve => setTimeout(resolve, 1000)); + console.log('[Resend Verification] ✅ Cleanup wait completed'); + + // STEP 5: Add the verification tag to restart the process + const verificationTag = `${params.method}-code`; + await this.addContactTags({ + contactId: params.contactId, + tags: [verificationTag] + }); + console.log(`[Resend Verification] ✅ Added ${verificationTag} tag to restart verification`); + + // STEP 6: Re-add to workflow to trigger new verification + if (workflowId) { + try { + await this.addContactToWorkflow({ + contactId: params.contactId, + workflowId: workflowId, + eventStartTime: this.getGHLTimestamp() + }); + console.log('[Resend Verification] ✅ Re-added contact to verification workflow'); + } catch (workflowError) { + console.error('[Resend Verification] ❌ Failed to re-add to workflow:', workflowError); + return { + success: false, + message: 'Failed to restart verification workflow. Please try again.', + error: workflowError instanceof Error ? workflowError.message : String(workflowError) + }; + } + } + + return { + success: true, + message: `${params.method.charAt(0).toUpperCase() + params.method.slice(1)} verification code resent successfully`, + contactId: params.contactId, + method: params.method, + instructions: `Please check your ${params.method === 'email' ? 'email' : params.method.toUpperCase()} for the new verification code` + }; + + } catch (error) { + console.error('[Resend Verification] Error:', error); + return { + success: false, + message: 'Failed to resend verification code', + error: error instanceof Error ? error.message : String(error) + }; + } + } + + private async checkVerificationStatus(params: { email: string }) { + try { + const contacts = await this.searchContacts({ query: params.email, limit: 1 }); + + if (contacts.contacts.length === 0) { + return { + verified: false, + exists: false, + message: 'Contact not found' + }; + } + + const contact = contacts.contacts[0]; + const hasVerifiedTag = contact.tags?.includes('verified-email') || false; + const verificationField = contact.customFields?.find(field => field.id === 'verification_timestamp'); + const verificationDate = verificationField?.field_value as string; + + return { + verified: hasVerifiedTag, + exists: true, + contactId: contact.id, + verificationDate: verificationDate, + tags: contact.tags + }; + } catch (error) { + console.error('[OTP] Check status error:', error); + return { + verified: false, + exists: false, + message: 'Error checking verification status' + }; + } + } } \ No newline at end of file diff --git a/src/types/ghl-types.ts b/src/types/ghl-types.ts index 0d465170..a38b3e6a 100644 --- a/src/types/ghl-types.ts +++ b/src/types/ghl-types.ts @@ -440,6 +440,7 @@ export interface MCPUpdateContactParams { email?: string; phone?: string; tags?: string[]; + customFields?: Record; // Field ID -> Field Value mapping } export interface MCPAddContactTagsParams { diff --git a/src/vapi-mcp-server.ts b/src/vapi-mcp-server.ts new file mode 100644 index 00000000..77f526f4 --- /dev/null +++ b/src/vapi-mcp-server.ts @@ -0,0 +1,889 @@ +/** + * GoHighLevel MCP Server for Vapi AI + * Streamable HTTP protocol (not SSE) for Vapi integration + */ + +import express from 'express'; +import cors from 'cors'; +import * as dotenv from 'dotenv'; + +import { GHLApiClient } from './clients/ghl-api-client'; +import { ContactTools } from './tools/contact-tools'; +import { ConversationTools } from './tools/conversation-tools'; +import { BlogTools } from './tools/blog-tools'; +import { OpportunityTools } from './tools/opportunity-tools'; +import { CalendarTools } from './tools/calendar-tools'; +import { EmailTools } from './tools/email-tools'; +import { LocationTools } from './tools/location-tools'; +import { EmailISVTools } from './tools/email-isv-tools'; +import { SocialMediaTools } from './tools/social-media-tools'; +import { MediaTools } from './tools/media-tools'; +import { ObjectTools } from './tools/object-tools'; +import { AssociationTools } from './tools/association-tools'; +import { CustomFieldV2Tools } from './tools/custom-field-v2-tools'; +import { WorkflowTools } from './tools/workflow-tools'; +import { SurveyTools } from './tools/survey-tools'; +import { StoreTools } from './tools/store-tools'; +import { ProductsTools } from './tools/products-tools.js'; +import { InvoicesTools } from './tools/invoices-tools'; +import { GHLConfig } from './types/ghl-types'; + +// Load environment variables +dotenv.config(); + +/** + * Vapi MCP Server class - Streamable HTTP protocol + */ +class VapiMCPServer { + private app: express.Application; + private ghlClient: GHLApiClient; + private contactTools: ContactTools; + private conversationTools: ConversationTools; + private blogTools: BlogTools; + private opportunityTools: OpportunityTools; + private calendarTools: CalendarTools; + private emailTools: EmailTools; + private locationTools: LocationTools; + private emailISVTools: EmailISVTools; + private socialMediaTools: SocialMediaTools; + private mediaTools: MediaTools; + private objectTools: ObjectTools; + private associationTools: AssociationTools; + private customFieldV2Tools: CustomFieldV2Tools; + private workflowTools: WorkflowTools; + private surveyTools: SurveyTools; + private storeTools: StoreTools; + private productsTools: ProductsTools; + private invoicesTools: InvoicesTools; + private port: number; + + constructor() { + // Railway provides PORT env var - use it! + this.port = parseInt(process.env.PORT || process.env.VAPI_MCP_PORT || '3000'); + + console.log(`[Vapi MCP] Using PORT: ${this.port} (from env: ${process.env.PORT || 'default'})`); + + // Initialize Express app + this.app = express(); + this.setupExpress(); + + // Initialize GHL API client + this.ghlClient = this.initializeGHLClient(); + + // Initialize all tools + this.contactTools = new ContactTools(this.ghlClient); + this.conversationTools = new ConversationTools(this.ghlClient); + this.blogTools = new BlogTools(this.ghlClient); + this.opportunityTools = new OpportunityTools(this.ghlClient); + this.calendarTools = new CalendarTools(this.ghlClient); + this.emailTools = new EmailTools(this.ghlClient); + this.locationTools = new LocationTools(this.ghlClient); + this.emailISVTools = new EmailISVTools(this.ghlClient); + this.socialMediaTools = new SocialMediaTools(this.ghlClient); + this.mediaTools = new MediaTools(this.ghlClient); + this.objectTools = new ObjectTools(this.ghlClient); + this.associationTools = new AssociationTools(this.ghlClient); + this.customFieldV2Tools = new CustomFieldV2Tools(this.ghlClient); + this.workflowTools = new WorkflowTools(this.ghlClient); + this.surveyTools = new SurveyTools(this.ghlClient); + this.storeTools = new StoreTools(this.ghlClient); + this.productsTools = new ProductsTools(this.ghlClient); + this.invoicesTools = new InvoicesTools(this.ghlClient); + + this.setupRoutes(); + } + + /** + * Setup Express middleware and configuration + */ + private setupExpress(): void { + // Enable CORS for Vapi integration + this.app.use(cors({ + origin: '*', + methods: ['GET', 'POST', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Call-Id', 'X-Chat-Id', 'X-Session-Id'], + credentials: false + })); + + // JSON parsing middleware + this.app.use(express.json({ limit: '10mb' })); + + // Request logging with Vapi headers + this.app.use((req, res, next) => { + const callId = req.headers['x-call-id'] || 'unknown'; + const chatId = req.headers['x-chat-id'] || 'unknown'; + const sessionId = req.headers['x-session-id'] || 'unknown'; + + console.log(`[Vapi MCP] ${req.method} ${req.path}`); + console.log(`[Vapi MCP] Call: ${callId}, Chat: ${chatId}, Session: ${sessionId}`); + console.log(`[Vapi MCP] Time: ${new Date().toISOString()}`); + next(); + }); + } + + /** + * Initialize GoHighLevel API client + */ + private initializeGHLClient(): GHLApiClient { + const config: GHLConfig = { + accessToken: process.env.GHL_API_KEY || '', + baseUrl: process.env.GHL_BASE_URL || 'https://services.leadconnectorhq.com', + version: '2021-07-28', + locationId: process.env.GHL_LOCATION_ID || '' + }; + + // Validate required configuration + if (!config.accessToken) { + throw new Error('GHL_API_KEY environment variable is required'); + } + + if (!config.locationId) { + throw new Error('GHL_LOCATION_ID environment variable is required'); + } + + console.log('[Vapi MCP] Initializing GHL API client...'); + console.log(`[Vapi MCP] Base URL: ${config.baseUrl}`); + console.log(`[Vapi MCP] Location ID: ${config.locationId}`); + + return new GHLApiClient(config); + } + + /** + * Setup HTTP routes for Vapi + */ + private setupRoutes(): void { + // Health check endpoint + this.app.get('/health', async (req, res) => { + try { + const testResponse = await this.ghlClient.getLocationById(this.ghlClient.getConfig().locationId); + + res.json({ + status: 'healthy', + server: 'vapi-ghl-mcp-server', + version: '1.0.0', + timestamp: new Date().toISOString(), + protocol: 'streamable-http', + tools: this.getToolsCount(), + ghl: { + connected: testResponse.success, + locationId: this.ghlClient.getConfig().locationId, + locationName: testResponse.data?.location?.name || 'Unknown', + baseUrl: this.ghlClient.getConfig().baseUrl + } + }); + } catch (error) { + res.status(500).json({ + status: 'unhealthy', + server: 'vapi-ghl-mcp-server', + version: '1.0.0', + timestamp: new Date().toISOString(), + protocol: 'streamable-http', + error: error instanceof Error ? error.message : 'Unknown error' + }); + } + }); + + // Root endpoint - server info (GET) + this.app.get('/', (req, res) => { + res.json({ + name: 'GoHighLevel MCP Server for Vapi', + version: '1.0.0', + protocol: 'streamable-http', + status: 'running', + endpoints: { + health: '/health', + mcp: '/ (POST with MCP protocol messages)' + }, + tools: this.getToolsCount(), + documentation: 'https://github.com/your-repo/ghl-mcp-server' + }); + }); + + // Root endpoint - Handle MCP protocol messages (POST) + // Vapi sends MCP requests to root "/" path + this.app.post('/', async (req: express.Request, res: express.Response): Promise => { + const method = req.body?.method; + console.log(`[Vapi MCP] Root POST received - Method: ${method || 'none'}`); + console.log(`[Vapi MCP] Full request body:`, JSON.stringify(req.body, null, 2)); + + if (!method) { + console.log('[Vapi MCP] No method in request body'); + console.log('[Vapi MCP] Body was:', req.body); + res.json({ + jsonrpc: '2.0', + error: { + code: -32600, + message: 'Invalid Request - no method specified' + } + }); + return; + } + + // Handle MCP initialize + if (method === 'initialize') { + const clientVersion = req.body?.params?.protocolVersion || '2024-11-05'; + const supportedVersions = ['2024-11-05', '2025-03-26']; + const protocolVersion = supportedVersions.includes(clientVersion) ? clientVersion : '2024-11-05'; + + const response = { + jsonrpc: '2.0', + id: req.body?.id !== undefined ? req.body.id : 1, + result: { + protocolVersion: protocolVersion, + capabilities: { + tools: {} + }, + serverInfo: { + name: 'vapi-ghl-mcp-server', + version: '1.0.0' + } + } + }; + + console.log('[Vapi MCP] Sending initialize response with protocol:', protocolVersion); + console.log('[Vapi MCP] Response:', JSON.stringify(response, null, 2)); + res.json(response); + return; + } + + // Handle MCP initialized notification (no response needed) + if (method === 'initialized' || method === 'notifications/initialized') { + console.log('[Vapi MCP] Received initialized notification - handshake complete!'); + // Notifications don't need a response, but we acknowledge receipt + res.status(200).send(); + return; + } + + // Handle MCP tools/list + if (method === 'tools/list') { + console.log('[Vapi MCP] Tools list requested at root'); + // Use FILTERED tools for Vapi to prevent AI overwhelm + const tools = this.getFilteredToolDefinitions(); + + const response = { + jsonrpc: '2.0', + id: req.body?.id || 1, + result: { + tools: tools + } + }; + + console.log(`[Vapi MCP] Returning ${tools.length} FILTERED tools (essential only)`); + res.json(response); + return; + } + + // Handle MCP tools/call + if (method === 'tools/call') { + const { name, arguments: args } = req.body?.params || {}; + console.log(`[Vapi MCP] Tool call at root: ${name}`); + + try { + const result = await this.executeToolCall(name, args); + + const response = { + jsonrpc: '2.0', + id: req.body?.id || 1, + result: { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + } + }; + + console.log(`[Vapi MCP] Tool ${name} executed successfully`); + res.json(response); + return; + } catch (error) { + console.error(`[Vapi MCP] Tool ${name} failed:`, error); + + const errorResponse = { + jsonrpc: '2.0', + id: req.body?.id || 1, + error: { + code: -32603, + message: error instanceof Error ? error.message : 'Tool execution failed' + } + }; + + res.status(500).json(errorResponse); + return; + } + } + + // Unknown method + console.log(`[Vapi MCP] Unknown method at root: ${method}`); + res.status(400).json({ + jsonrpc: '2.0', + id: req.body?.id || 1, + error: { + code: -32601, + message: `Unknown method: ${method}` + } + }); + }); + + // MCP Initialize endpoint (legacy /mcp/* paths) + this.app.post('/mcp/initialize', (req, res) => { + console.log('[Vapi MCP] Initialize request received'); + console.log('[Vapi MCP] Request body:', JSON.stringify(req.body, null, 2)); + + const clientVersion = req.body?.params?.protocolVersion || '2024-11-05'; + const supportedVersions = ['2024-11-05', '2025-03-26']; + const protocolVersion = supportedVersions.includes(clientVersion) ? clientVersion : '2024-11-05'; + + const response = { + jsonrpc: '2.0', + id: req.body?.id || 1, + result: { + protocolVersion: protocolVersion, + capabilities: { + tools: {} + }, + serverInfo: { + name: 'vapi-ghl-mcp-server', + version: '1.0.0' + } + } + }; + + console.log('[Vapi MCP] Sending initialize response:', JSON.stringify(response, null, 2)); + res.json(response); + }); + + // MCP Tools List endpoint (legacy) + this.app.post('/mcp/tools/list', (req, res) => { + console.log('[Vapi MCP] Tools list request received (legacy endpoint)'); + + try { + // Use FILTERED tools for Vapi + const tools = this.getFilteredToolDefinitions(); + + const response = { + jsonrpc: '2.0', + id: req.body?.id || 1, + result: { + tools: tools + } + }; + + console.log(`[Vapi MCP] Returning ${tools.length} FILTERED tools`); + res.json(response); + } catch (error) { + console.error('[Vapi MCP] Error listing tools:', error); + + const errorResponse = { + jsonrpc: '2.0', + id: req.body?.id || 1, + error: { + code: -32603, + message: error instanceof Error ? error.message : 'Failed to list tools' + } + }; + + res.status(500).json(errorResponse); + } + }); + + // MCP Tool Call endpoint + this.app.post('/mcp/tools/call', async (req, res) => { + const { name, arguments: args } = req.body?.params || {}; + const callId = req.headers['x-call-id'] || 'unknown'; + const chatId = req.headers['x-chat-id'] || 'unknown'; + + console.log(`[Vapi MCP] Tool call: ${name}`); + console.log(`[Vapi MCP] Call ID: ${callId}, Chat ID: ${chatId}`); + console.log(`[Vapi MCP] Arguments:`, JSON.stringify(args, null, 2)); + + try { + const result = await this.executeToolCall(name, args); + + const response = { + jsonrpc: '2.0', + id: req.body?.id || 1, + result: { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2) + } + ] + } + }; + + console.log(`[Vapi MCP] Tool ${name} executed successfully`); + res.json(response); + } catch (error) { + console.error(`[Vapi MCP] Tool ${name} execution failed:`, error); + + const errorResponse = { + jsonrpc: '2.0', + id: req.body?.id || 1, + error: { + code: -32603, + message: error instanceof Error ? error.message : 'Tool execution failed' + } + }; + + res.status(500).json(errorResponse); + } + }); + + // Legacy endpoints for compatibility (GET variants) + this.app.get('/mcp/tools/list', (req, res) => { + try { + const tools = this.getAllToolDefinitions(); + res.json({ tools: tools, count: tools.length }); + } catch (error) { + res.status(500).json({ error: 'Failed to list tools' }); + } + }); + + // Debug endpoint for Vapi troubleshooting + this.app.all('/debug', (req, res) => { + console.log(`[Vapi Debug] ${req.method} request`); + console.log(`[Vapi Debug] Headers:`, JSON.stringify(req.headers, null, 2)); + console.log(`[Vapi Debug] Query:`, req.query); + console.log(`[Vapi Debug] Body:`, JSON.stringify(req.body, null, 2)); + + res.json({ + method: req.method, + headers: req.headers, + query: req.query, + body: req.body, + timestamp: new Date().toISOString() + }); + }); + } + + /** + * Execute a tool call + */ + private async executeToolCall(name: string, args: any) { + // Route to appropriate tool handler based on tool name + if (this.isContactTool(name)) { + return await this.contactTools.executeTool(name, args || {}); + } else if (this.isConversationTool(name)) { + return await this.conversationTools.executeTool(name, args || {}); + } else if (this.isBlogTool(name)) { + return await this.blogTools.executeTool(name, args || {}); + } else if (this.isOpportunityTool(name)) { + return await this.opportunityTools.executeTool(name, args || {}); + } else if (this.isCalendarTool(name)) { + return await this.calendarTools.executeTool(name, args || {}); + } else if (this.isEmailTool(name)) { + return await this.emailTools.executeTool(name, args || {}); + } else if (this.isLocationTool(name)) { + return await this.locationTools.executeTool(name, args || {}); + } else if (this.isEmailISVTool(name)) { + return await this.emailISVTools.executeTool(name, args || {}); + } else if (this.isSocialMediaTool(name)) { + return await this.socialMediaTools.executeTool(name, args || {}); + } else if (this.isMediaTool(name)) { + return await this.mediaTools.executeTool(name, args || {}); + } else if (this.isObjectTool(name)) { + return await this.objectTools.executeTool(name, args || {}); + } else if (this.isAssociationTool(name)) { + return await this.associationTools.executeAssociationTool(name, args || {}); + } else if (this.isCustomFieldV2Tool(name)) { + return await this.customFieldV2Tools.executeCustomFieldV2Tool(name, args || {}); + } else if (this.isWorkflowTool(name)) { + return await this.workflowTools.executeWorkflowTool(name, args || {}); + } else if (this.isSurveyTool(name)) { + return await this.surveyTools.executeSurveyTool(name, args || {}); + } else if (this.isStoreTool(name)) { + return await this.storeTools.executeStoreTool(name, args || {}); + } else if (this.isProductsTool(name)) { + return await this.productsTools.executeProductsTool(name, args || {}); + } else if (this.isInvoiceTool(name)) { + return await this.invoicesTools.handleToolCall(name, args || {}); + } else { + throw new Error(`Unknown tool: ${name}`); + } + } + + /** + * Get FILTERED tool definitions for Vapi (essential tools only) + * Vapi AI gets overwhelmed - limit to MAX 20 tools for optimal performance + */ + private getFilteredToolDefinitions() { + // CRITICAL: Tested working limit = EXACTLY 16 tools (20+ causes AI freeze) + const essentialToolNames = [ + // Contact Management (7 tools) - NO delete_contact! + 'search_contacts', + 'get_contact', + 'create_contact', + 'upsert_contact', + 'update_contact', + 'add_contact_tags', + 'remove_contact_tags', + + // Verification (6 tools) - Complete verification flow + 'start_email_verification', + 'start_sms_verification', + 'start_whatsapp_verification', + 'verify_code', + 'resend_verification_code', + 'check_verification_status', + + // Calendar & Appointments (4 tools) + 'get_free_slots', + 'create_appointment', + 'get_contact_appointments', + 'update_appointment' + ]; + // TOTAL: 17 tools (16 worked, added 3 verification, removed 2 communication) + + // Get all tools + const allTools = this.getAllToolDefinitions(); + + // Filter to only essential tools + const filteredTools = allTools.filter(tool => essentialToolNames.includes(tool.name)); + + console.log(`[Vapi MCP] Filtered tools: ${filteredTools.length}/${allTools.length} (PROVEN WORKING: 16)`); + console.log(`[Vapi MCP] Security: delete_contact EXCLUDED`); + + return filteredTools; + } + + /** + * Get all tool definitions (for non-Vapi clients or debugging) + */ + private getAllToolDefinitions() { + const contactTools = this.contactTools.getToolDefinitions(); + const conversationTools = this.conversationTools.getToolDefinitions(); + const blogTools = this.blogTools.getToolDefinitions(); + const opportunityTools = this.opportunityTools.getToolDefinitions(); + const calendarTools = this.calendarTools.getToolDefinitions(); + const emailTools = this.emailTools.getToolDefinitions(); + const locationTools = this.locationTools.getToolDefinitions(); + const emailISVTools = this.emailISVTools.getToolDefinitions(); + const socialMediaTools = this.socialMediaTools.getTools(); + const mediaTools = this.mediaTools.getToolDefinitions(); + const objectTools = this.objectTools.getToolDefinitions(); + const associationTools = this.associationTools.getTools(); + const customFieldV2Tools = this.customFieldV2Tools.getTools(); + const workflowTools = this.workflowTools.getTools(); + const surveyTools = this.surveyTools.getTools(); + const storeTools = this.storeTools.getTools(); + const productsTools = this.productsTools.getTools(); + const invoicesTools = this.invoicesTools.getTools(); + + return [ + ...contactTools, + ...conversationTools, + ...blogTools, + ...opportunityTools, + ...calendarTools, + ...emailTools, + ...locationTools, + ...emailISVTools, + ...socialMediaTools, + ...mediaTools, + ...objectTools, + ...associationTools, + ...customFieldV2Tools, + ...workflowTools, + ...surveyTools, + ...storeTools, + ...productsTools, + ...invoicesTools + ]; + } + + /** + * Get tools count summary + */ + private getToolsCount() { + return { + contact: this.contactTools.getToolDefinitions().length, + conversation: this.conversationTools.getToolDefinitions().length, + blog: this.blogTools.getToolDefinitions().length, + opportunity: this.opportunityTools.getToolDefinitions().length, + calendar: this.calendarTools.getToolDefinitions().length, + email: this.emailTools.getToolDefinitions().length, + location: this.locationTools.getToolDefinitions().length, + emailISV: this.emailISVTools.getToolDefinitions().length, + socialMedia: this.socialMediaTools.getTools().length, + media: this.mediaTools.getToolDefinitions().length, + objects: this.objectTools.getToolDefinitions().length, + associations: this.associationTools.getTools().length, + customFieldsV2: this.customFieldV2Tools.getTools().length, + workflows: this.workflowTools.getTools().length, + surveys: this.surveyTools.getTools().length, + store: this.storeTools.getTools().length, + products: this.productsTools.getTools().length, + invoices: this.invoicesTools.getTools().length, + total: this.contactTools.getToolDefinitions().length + + this.conversationTools.getToolDefinitions().length + + this.blogTools.getToolDefinitions().length + + this.opportunityTools.getToolDefinitions().length + + this.calendarTools.getToolDefinitions().length + + this.emailTools.getToolDefinitions().length + + this.locationTools.getToolDefinitions().length + + this.emailISVTools.getToolDefinitions().length + + this.socialMediaTools.getTools().length + + this.mediaTools.getToolDefinitions().length + + this.objectTools.getToolDefinitions().length + + this.associationTools.getTools().length + + this.customFieldV2Tools.getTools().length + + this.workflowTools.getTools().length + + this.surveyTools.getTools().length + + this.storeTools.getTools().length + + this.productsTools.getTools().length + + this.invoicesTools.getTools().length + }; + } + + // Tool name validation helpers (same as original server) + private isContactTool(toolName: string): boolean { + const contactToolNames = [ + 'create_contact', 'search_contacts', 'get_contact', 'update_contact', + 'add_contact_tags', 'remove_contact_tags', 'delete_contact', + 'start_email_verification', 'start_sms_verification', 'start_whatsapp_verification', + 'verify_code', 'resend_verification_code', 'check_verification_status', + 'get_contact_tasks', 'create_contact_task', 'get_contact_task', 'update_contact_task', + 'delete_contact_task', 'update_task_completion', + 'get_contact_notes', 'create_contact_note', 'get_contact_note', 'update_contact_note', + 'delete_contact_note', 'upsert_contact', 'get_duplicate_contact', 'get_contacts_by_business', + 'get_contact_appointments', 'bulk_update_contact_tags', 'bulk_update_contact_business', + 'add_contact_followers', 'remove_contact_followers', 'add_contact_to_campaign', + 'remove_contact_from_campaign', 'remove_contact_from_all_campaigns', + 'add_contact_to_workflow', 'remove_contact_from_workflow' + ]; + return contactToolNames.includes(toolName); + } + + private isConversationTool(toolName: string): boolean { + const conversationToolNames = [ + 'send_sms', 'send_email', 'search_conversations', 'get_conversation', + 'create_conversation', 'update_conversation', 'delete_conversation', 'get_recent_messages', + 'get_email_message', 'get_message', 'upload_message_attachments', 'update_message_status', + 'add_inbound_message', 'add_outbound_call', 'get_message_recording', 'get_message_transcription', + 'download_transcription', 'cancel_scheduled_message', 'cancel_scheduled_email', 'live_chat_typing' + ]; + return conversationToolNames.includes(toolName); + } + + private isBlogTool(toolName: string): boolean { + const blogToolNames = [ + 'create_blog_post', 'update_blog_post', 'get_blog_posts', 'get_blog_sites', + 'get_blog_authors', 'get_blog_categories', 'check_url_slug' + ]; + return blogToolNames.includes(toolName); + } + + private isOpportunityTool(toolName: string): boolean { + const opportunityToolNames = [ + 'search_opportunities', 'get_pipelines', 'get_opportunity', 'create_opportunity', + 'update_opportunity_status', 'delete_opportunity', 'update_opportunity', + 'upsert_opportunity', 'add_opportunity_followers', 'remove_opportunity_followers' + ]; + return opportunityToolNames.includes(toolName); + } + + private isCalendarTool(toolName: string): boolean { + const calendarToolNames = [ + 'get_calendar_groups', 'create_calendar_group', 'validate_group_slug', + 'update_calendar_group', 'delete_calendar_group', 'disable_calendar_group', + 'get_calendars', 'create_calendar', 'get_calendar', 'update_calendar', 'delete_calendar', + 'get_calendar_events', 'get_free_slots', 'create_appointment', 'get_appointment', + 'update_appointment', 'delete_appointment', 'get_appointment_notes', 'create_appointment_note', + 'update_appointment_note', 'delete_appointment_note', 'get_calendar_resources', + 'get_calendar_resource_by_id', 'update_calendar_resource', 'delete_calendar_resource', + 'get_calendar_notifications', 'create_calendar_notification', 'update_calendar_notification', + 'delete_calendar_notification', 'create_block_slot', 'update_block_slot', 'get_blocked_slots', 'delete_blocked_slot' + ]; + return calendarToolNames.includes(toolName); + } + + private isEmailTool(toolName: string): boolean { + const emailToolNames = [ + 'get_email_campaigns', 'create_email_template', 'get_email_templates', + 'update_email_template', 'delete_email_template' + ]; + return emailToolNames.includes(toolName); + } + + private isLocationTool(toolName: string): boolean { + const locationToolNames = [ + 'search_locations', 'get_location', 'create_location', 'update_location', 'delete_location', + 'get_location_tags', 'create_location_tag', 'get_location_tag', 'update_location_tag', 'delete_location_tag', + 'search_location_tasks', 'get_location_custom_fields', 'create_location_custom_field', 'get_location_custom_field', + 'update_location_custom_field', 'delete_location_custom_field', 'get_location_custom_values', + 'create_location_custom_value', 'get_location_custom_value', 'update_location_custom_value', + 'delete_location_custom_value', 'get_location_templates', 'delete_location_template', 'get_timezones' + ]; + return locationToolNames.includes(toolName); + } + + private isEmailISVTool(toolName: string): boolean { + return ['verify_email'].includes(toolName); + } + + private isSocialMediaTool(toolName: string): boolean { + const socialMediaToolNames = [ + 'search_social_posts', 'create_social_post', 'get_social_post', 'update_social_post', + 'delete_social_post', 'bulk_delete_social_posts', 'get_social_accounts', 'delete_social_account', + 'upload_social_csv', 'get_csv_upload_status', 'set_csv_accounts', 'get_social_categories', + 'get_social_category', 'get_social_tags', 'get_social_tags_by_ids', 'start_social_oauth', 'get_platform_accounts' + ]; + return socialMediaToolNames.includes(toolName); + } + + private isMediaTool(toolName: string): boolean { + return ['get_media_files', 'upload_media_file', 'delete_media_file'].includes(toolName); + } + + private isObjectTool(toolName: string): boolean { + const objectToolNames = [ + 'get_all_objects', 'create_object_schema', 'get_object_schema', 'update_object_schema', + 'create_object_record', 'get_object_record', 'update_object_record', 'delete_object_record', 'search_object_records' + ]; + return objectToolNames.includes(toolName); + } + + private isAssociationTool(toolName: string): boolean { + const associationToolNames = [ + 'ghl_get_all_associations', 'ghl_create_association', 'ghl_get_association_by_id', + 'ghl_update_association', 'ghl_delete_association', 'ghl_get_association_by_key', + 'ghl_get_association_by_object_key', 'ghl_create_relation', 'ghl_get_relations_by_record', 'ghl_delete_relation' + ]; + return associationToolNames.includes(toolName); + } + + private isCustomFieldV2Tool(toolName: string): boolean { + const customFieldV2ToolNames = [ + 'ghl_get_custom_field_by_id', 'ghl_create_custom_field', 'ghl_update_custom_field', + 'ghl_delete_custom_field', 'ghl_get_custom_fields_by_object_key', 'ghl_create_custom_field_folder', + 'ghl_update_custom_field_folder', 'ghl_delete_custom_field_folder' + ]; + return customFieldV2ToolNames.includes(toolName); + } + + private isWorkflowTool(toolName: string): boolean { + return ['ghl_get_workflows'].includes(toolName); + } + + private isSurveyTool(toolName: string): boolean { + return ['ghl_get_surveys', 'ghl_get_survey_submissions'].includes(toolName); + } + + private isStoreTool(toolName: string): boolean { + const storeToolNames = [ + 'ghl_create_shipping_zone', 'ghl_list_shipping_zones', 'ghl_get_shipping_zone', + 'ghl_update_shipping_zone', 'ghl_delete_shipping_zone', 'ghl_get_available_shipping_rates', + 'ghl_create_shipping_rate', 'ghl_list_shipping_rates', 'ghl_get_shipping_rate', + 'ghl_update_shipping_rate', 'ghl_delete_shipping_rate', 'ghl_create_shipping_carrier', + 'ghl_list_shipping_carriers', 'ghl_get_shipping_carrier', 'ghl_update_shipping_carrier', + 'ghl_delete_shipping_carrier', 'ghl_create_store_setting', 'ghl_get_store_setting' + ]; + return storeToolNames.includes(toolName); + } + + private isProductsTool(toolName: string): boolean { + const productsToolNames = [ + 'ghl_create_product', 'ghl_list_products', 'ghl_get_product', 'ghl_update_product', + 'ghl_delete_product', 'ghl_bulk_update_products', 'ghl_create_price', 'ghl_list_prices', + 'ghl_get_price', 'ghl_update_price', 'ghl_delete_price', 'ghl_list_inventory', + 'ghl_update_inventory', 'ghl_get_product_store_stats', 'ghl_update_product_store', + 'ghl_create_product_collection', 'ghl_list_product_collections', 'ghl_get_product_collection', + 'ghl_update_product_collection', 'ghl_delete_product_collection', 'ghl_list_product_reviews', + 'ghl_get_reviews_count', 'ghl_update_product_review', 'ghl_delete_product_review', 'ghl_bulk_update_product_reviews' + ]; + return productsToolNames.includes(toolName); + } + + private isInvoiceTool(toolName: string): boolean { + const invoiceToolNames = [ + 'create_invoice_template', 'list_invoice_templates', 'get_invoice_template', + 'update_invoice_template', 'delete_invoice_template', 'create_invoice_schedule', + 'list_invoice_schedules', 'get_invoice_schedule', 'create_invoice', + 'list_invoices', 'get_invoice', 'send_invoice', 'create_invoice_from_estimate', + 'generate_invoice_number' + ]; + return invoiceToolNames.includes(toolName); + } + + /** + * Start the server + */ + async start(): Promise { + console.log('🚀 Starting Vapi GoHighLevel MCP Server...'); + console.log('=========================================='); + + try { + // Test GHL API connection + await this.testGHLConnection(); + + // Start HTTP server + this.app.listen(this.port, '0.0.0.0', () => { + console.log('✅ Vapi GoHighLevel MCP Server started successfully!'); + console.log(`🌐 Server running on: http://0.0.0.0:${this.port}`); + console.log(`🔗 MCP URL for Vapi: http://0.0.0.0:${this.port}`); + console.log(`📋 Total Tools: ${this.getToolsCount().total}`); + console.log(`🎯 Protocol: Streamable HTTP (Vapi compatible)`); + console.log('=========================================='); + }); + + } catch (error) { + console.error('❌ Failed to start Vapi MCP Server:', error); + process.exit(1); + } + } + + /** + * Test GHL API connection + */ + private async testGHLConnection(): Promise { + try { + console.log('[Vapi MCP] Testing GHL API connection...'); + + const result = await this.ghlClient.testConnection(); + + console.log('[Vapi MCP] ✅ GHL API connection successful'); + console.log(`[Vapi MCP] Connected to location: ${result.data?.locationId}`); + } catch (error) { + console.error('[Vapi MCP] ❌ GHL API connection failed:', error); + throw new Error(`Failed to connect to GHL API: ${error}`); + } + } +} + +/** + * Handle graceful shutdown + */ +function setupGracefulShutdown(): void { + const shutdown = (signal: string) => { + console.log(`\n[Vapi MCP] Received ${signal}, shutting down gracefully...`); + process.exit(0); + }; + + process.on('SIGINT', () => shutdown('SIGINT')); + process.on('SIGTERM', () => shutdown('SIGTERM')); +} + +/** + * Main entry point + */ +async function main(): Promise { + try { + setupGracefulShutdown(); + + const server = new VapiMCPServer(); + await server.start(); + + } catch (error) { + console.error('💥 Fatal error:', error); + process.exit(1); + } +} + +// Start the server +main().catch((error) => { + console.error('Unhandled error:', error); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json index 355b7a25..4dae5935 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,15 +2,15 @@ "compilerOptions": { "target": "ES2022", "module": "NodeNext", - "moduleResolution": "NodeNext", + "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "outDir": "./dist", - "types": ["node", "jest"] + "types": ["node"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "tests/**/*"] -} \ No newline at end of file +} \ No newline at end of file