Skip to main content
Glama
RohitashAery

GHL MCP Server

by RohitashAery
README.md
# GHL MCP Server

A production-grade **Model Context Protocol (MCP) server** that wraps the GoHighLevel (GHL) API v2, enabling Claude and other MCP clients to interact with GHL CRM as structured tools — creating contacts, sending messages, managing pipelines, booking appointments, and more.

***

## Overview

This server exposes **113 tools** as MCP tools — 111 GHL API tools across 17 modules, plus 2 AI agent tools that chain multiple GHL operations autonomously.

| Module          | Tools   | Description                                                        |
| --------------- | ------- | ------------------------------------------------------------------ |
| Contacts        | 14      | CRUD, search, tags, notes, tasks, upsert                           |
| Conversations   | 7       | List threads, send SMS/email, get messages, mark read, reports     |
| Opportunities   | 9       | Pipeline management, stage moves, CRUD                             |
| Calendars       | 15      | Calendars, calendar groups, slots, book/update/cancel appointments |
| Payments        | 11      | Products, invoices, transactions, subscriptions                    |
| Workflows       | 3       | List, enroll/remove contacts from automations                      |
| Forms & Surveys | 4       | List forms/surveys, get submissions                                |
| Users           | 5       | CRUD team members                                                  |
| Locations       | 16      | Sub-account management, custom values, custom fields, tags         |
| Media           | 3       | Upload, list, delete media library files                           |
| Links           | 5       | Custom tracking link management                                    |
| Blogs           | 5       | Blog post management                                               |
| Funnels         | 5       | Funnel and funnel-page management                                  |
| Snapshots       | 2       | View cloneable account snapshots                                   |
| Documents       | 4       | List, send docs/templates                                          |
| SaaS            | 3       | Plans, subscriptions, enable SaaS mode                             |
| **AI Agents**   | **2**   | Multi-step sales automation with LLM reasoning and HITL approval   |
| Webhooks        | inbound | Signature-validated event router                                   |

***

## Prerequisites

* Python 3.11+
* A GoHighLevel account with API access
* (For OAuth) A GHL Marketplace app with OAuth credentials

***

## Installation

```Shell
# 1. Clone the repository
git clone https://github.com/RohitashAery/ghl-mcp-server.git
cd ghl-mcp-server

# 2. Copy and configure environment
cp .env.example .env
# Edit .env with your credentials (see configuration section below)

# 3. Install dependencies
pip install -e .

# 4. Create data directory for OAuth token storage
mkdir -p data

# 5. Start the server
python main.py
```

The server starts at `http://localhost:8000`. Visit `http://localhost:8000/docs` for the Swagger UI.

***

## Configuration

All configuration is via environment variables (or `.env` file):

| Variable                | Default                                | Description                                                                                                  |
| ----------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `GHL_AUTH_MODE`         | `private`                              | Auth mode: `private` or `oauth`                                                                              |
| `GHL_PRIVATE_TOKEN`     | —                                      | Private integration token (private mode)                                                                     |
| `GHL_LOCATION_ID`       | —                                      | Default sub-account location ID                                                                              |
| `GHL_CLIENT_ID`         | —                                      | OAuth app client ID                                                                                          |
| `GHL_CLIENT_SECRET`     | —                                      | OAuth app client secret                                                                                      |
| `GHL_REDIRECT_URI`      | `http://localhost:8000/oauth/callback` | OAuth callback URL                                                                                           |
| `MCP_TRANSPORT`         | `http`                                 | Transport: `http` or `stdio`                                                                                 |
| `HOST`                  | `0.0.0.0`                              | HTTP server bind host                                                                                        |
| `PORT`                  | `8000`                                 | HTTP server port                                                                                             |
| `GHL_WEBHOOK_SECRET`    | —                                      | HMAC secret for webhook validation                                                                           |
| `LOG_LEVEL`             | `INFO`                                 | `DEBUG`, `INFO`, `WARNING`, `ERROR`                                                                          |
| `LOG_FORMAT`            | `json`                                 | `json` (production) or `console` (development)                                                               |
| `DB_PATH`               | `./data/tokens.db`                     | SQLite path for OAuth tokens                                                                                 |
| `ALLOWED_MODULES`       | `all`                                  | Comma-separated module names to expose, or `all`. Used for plan gating in hosted SaaS.                       |
| `ALLOWED_API_KEYS`      | *(empty)*                              | Comma-separated API keys required on `/mcp/sse`. Empty disables the check. Used for Claude seat enforcement. |
| `LLM_PROVIDER`          | `anthropic`                            | LLM backend for AI agents: `anthropic` or `openai`                                                           |
| `ANTHROPIC_API_KEY`     | —                                      | Anthropic API key (required if `LLM_PROVIDER=anthropic`)                                                     |
| `OPENAI_API_KEY`        | —                                      | OpenAI API key (required if `LLM_PROVIDER=openai`)                                                           |
| `LLM_MODEL`             | `claude-sonnet-4-6`                    | Model name passed to the LLM provider                                                                        |
| `AGENT_CHECKPOINTER_DB` | `./data/agent_checkpoints.db`          | SQLite path for LangGraph HITL checkpoint state                                                              |
| `CHROMA_PERSIST_DIR`    | `./data/chroma`                        | ChromaDB persistence directory for semantic vector search (Phase 2)                                          |

***

## SaaS Hosting (Multi-client)

This server supports running as a managed hosted service with multiple clients on shared infrastructure. Each client gets an isolated container with their own credentials and plan-gated tool set.

### Plan gating

Set `ALLOWED_MODULES` to a comma-separated list of module names:

```env
# Tier 1 — 48 tools
ALLOWED_MODULES=contacts,conversations,opportunities,pipelines,calendars,forms

# Tier 2 — 88 tools
ALLOWED_MODULES=contacts,conversations,opportunities,pipelines,calendars,payments,invoices,transactions,subscriptions,workflows,forms,surveys,users,media,links,blogs,funnels,documents

# Tier 3 — 113 tools (default: all GHL tools + AI agents)
ALLOWED_MODULES=all
```

Claude only sees the tools in the allowed modules — blocked modules are invisible.

### Claude seat enforcement

Set `ALLOWED_API_KEYS` to a comma-separated list of bearer tokens:

```env
ALLOWED_API_KEYS=key-abc123,key-xyz789
```

Clients include their key in Claude Desktop config:

```JSON
{
  "mcpServers": {
    "ghl": {
      "type": "sse",
      "url": "https://client.yourdomain.com/mcp/sse",
      "headers": { "Authorization": "Bearer key-abc123" }
    }
  }
}
```

See [docs/saas-deployment.md](docs/saas-deployment.md) for the full per-tier setup guide and [docs/aws-deployment.md](docs/aws-deployment.md) for the AWS ECS infrastructure guide.

***

## Private Token Setup

1. Log into GoHighLevel
2. Go to **Settings → Integrations → Private Integrations**
3. Click **Create New Integration**
4. Give it a name and select all required scopes
5. Copy the generated token
6. Set `GHL_PRIVATE_TOKEN=<token>` in your `.env`
7. Set `GHL_AUTH_MODE=private`

***

## OAuth Setup

### Step 1: Create a GHL Marketplace App

1. Go to [GHL Marketplace](https://marketplace.gohighlevel.com/)
2. Click **+ Create App**
3. Set the **Redirect URI** to your server's callback URL (e.g. `https://your-server.com/oauth/callback`)
4. Under **Scopes**, select all scopes listed in `.env.example`
5. Save — copy your **Client ID** and **Client Secret**

### Step 2: Configure your server

```Shell
GHL_AUTH_MODE=oauth
GHL_CLIENT_ID=your_client_id
GHL_CLIENT_SECRET=your_client_secret
GHL_REDIRECT_URI=http://localhost:8000/oauth/callback
```

### Step 3: Authorize

1. Start the server: `python main.py`
2. Open `http://localhost:8000/oauth/authorize` in your browser
3. Select the GHL location to authorize
4. After approval, you're redirected to `/oauth/callback`
5. Token is stored in SQLite — the server auto-refreshes before expiry

***

## Claude Desktop Setup (stdio mode)

Set `MCP_TRANSPORT=stdio` in your `.env`, then add to your Claude Desktop config:

**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`\
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`

```JSON
{
  "mcpServers": {
    "ghl": {
      "command": "python",
      "args": ["/absolute/path/to/ghl-mcp-server/main.py"],
      "env": {
        "MCP_TRANSPORT": "stdio",
        "GHL_AUTH_MODE": "private",
        "GHL_PRIVATE_TOKEN": "your_token_here",
        "GHL_LOCATION_ID": "your_location_id",
        "LOG_FORMAT": "console"
      }
    }
  }
}
```

Restart Claude Desktop. The GHL tools will appear in the tools panel.

***

## Claude.ai / N8N Setup (HTTP/SSE mode)

Set `MCP_TRANSPORT=http` and start the server. The SSE endpoint is:

```
http://your-server:8000/mcp/sse
```

**Claude.ai connector:** Add the SSE URL in Claude's connector settings.

**N8N:** Use the MCP Client node with the SSE endpoint URL.

**Swagger UI:** `http://your-server:8000/docs`

***

## All Tools Reference

### Contacts

| Tool                      | Description                     | Required Params                   |
| ------------------------- | ------------------------------- | --------------------------------- |
| `ghl_contacts_create`     | Create a new contact            | — (at least one of: email, phone) |
| `ghl_contacts_get`        | Get contact by ID               | `contactId`                       |
| `ghl_contacts_search`     | Search contacts                 | —                                 |
| `ghl_contacts_update`     | Update contact fields           | `contactId`, `fields`             |
| `ghl_contacts_delete`     | Delete a contact                | `contactId`                       |
| `ghl_contacts_add_tag`    | Add tags to contact             | `contactId`, `tags`               |
| `ghl_contacts_remove_tag` | Remove tags from contact        | `contactId`, `tags`               |
| `ghl_contacts_add_note`   | Add a note                      | `contactId`, `body`               |
| `ghl_contacts_list_notes` | List contact notes              | `contactId`                       |
| `ghl_contacts_add_task`   | Create a task                   | `contactId`, `title`, `dueDate`   |
| `ghl_contacts_list_tasks` | List contact tasks              | `contactId`                       |
| `ghl_contacts_upsert`     | Create or update by email/phone | `email` or `phone`                |

### Conversations

| Tool                             | Description               | Required Params                |
| -------------------------------- | ------------------------- | ------------------------------ |
| `ghl_conversations_list`         | List conversation threads | —                              |
| `ghl_conversations_get`          | Get a conversation        | `conversationId`               |
| `ghl_conversations_send_sms`     | Send SMS to contact       | `contactId`, `message`         |
| `ghl_conversations_send_email`   | Send email to contact     | `contactId`, `subject`, `body` |
| `ghl_conversations_get_messages` | Get messages in thread    | `conversationId`               |
| `ghl_conversations_mark_read`    | Mark conversation read    | `conversationId`               |

### Opportunities

| Tool                             | Description        | Required Params                              |
| -------------------------------- | ------------------ | -------------------------------------------- |
| `ghl_opportunities_list`         | List opportunities | —                                            |
| `ghl_opportunities_get`          | Get opportunity    | `opportunityId`                              |
| `ghl_opportunities_create`       | Create opportunity | `pipelineId`, `stageId`, `contactId`, `name` |
| `ghl_opportunities_update`       | Update opportunity | `opportunityId`, `fields`                    |
| `ghl_opportunities_update_stage` | Move to stage      | `opportunityId`, `stageId`                   |
| `ghl_opportunities_delete`       | Delete opportunity | `opportunityId`                              |
| `ghl_pipelines_list`             | List all pipelines | —                                            |

### Calendars & Appointments

| Tool                      | Description         | Required Params                                   |
| ------------------------- | ------------------- | ------------------------------------------------- |
| `ghl_calendars_list`      | List calendars      | —                                                 |
| `ghl_calendars_get_slots` | Get available slots | `calendarId`, `startDate`, `endDate`              |
| `ghl_appointments_list`   | List appointments   | —                                                 |
| `ghl_appointments_create` | Book appointment    | `calendarId`, `contactId`, `startTime`, `endTime` |
| `ghl_appointments_update` | Update appointment  | `appointmentId`, `fields`                         |
| `ghl_appointments_cancel` | Cancel appointment  | `appointmentId`                                   |

### Payments

| Tool                         | Description        | Required Params      |
| ---------------------------- | ------------------ | -------------------- |
| `ghl_payments_list_products` | List products      | —                    |
| `ghl_invoices_create`        | Create invoice     | `contactId`, `items` |
| `ghl_invoices_list`          | List invoices      | —                    |
| `ghl_transactions_list`      | List transactions  | —                    |
| `ghl_subscriptions_list`     | List subscriptions | —                    |
| `ghl_subscriptions_get`      | Get subscription   | `subscriptionId`     |

### Workflows

| Tool                           | Description                | Required Params           |
| ------------------------------ | -------------------------- | ------------------------- |
| `ghl_workflows_list`           | List workflows             | —                         |
| `ghl_workflows_add_contact`    | Enroll contact in workflow | `workflowId`, `contactId` |
| `ghl_workflows_remove_contact` | Remove from workflow       | `workflowId`, `contactId` |

### Forms

| Tool                        | Description          | Required Params |
| --------------------------- | -------------------- | --------------- |
| `ghl_forms_list`            | List forms           | —               |
| `ghl_forms_get_submissions` | Get form submissions | `formId`        |

### Users

| Tool               | Description       | Required Params                  |
| ------------------ | ----------------- | -------------------------------- |
| `ghl_users_list`   | List team members | —                                |
| `ghl_users_get`    | Get user by ID    | `userId`                         |
| `ghl_users_create` | Create user       | `firstName`, `lastName`, `email` |
| `ghl_users_update` | Update user       | `userId`, `fields`               |
| `ghl_users_delete` | Delete user       | `userId`                         |

### Locations

| Tool                   | Description       | Required Params        |
| ---------------------- | ----------------- | ---------------------- |
| `ghl_locations_list`   | List sub-accounts | `companyId`            |
| `ghl_locations_get`    | Get location      | `locationId`           |
| `ghl_locations_create` | Create location   | `companyId`, `name`    |
| `ghl_locations_update` | Update location   | `locationId`, `fields` |

### Documents

| Tool                 | Description    | Required Params           |
| -------------------- | -------------- | ------------------------- |
| `ghl_documents_list` | List documents | —                         |
| `ghl_documents_send` | Send document  | `documentId`, `contactId` |
| `ghl_templates_list` | List templates | —                         |
| `ghl_templates_send` | Send template  | `templateId`, `contactId` |

### SaaS

| Tool                        | Description              | Required Params |
| --------------------------- | ------------------------ | --------------- |
| `ghl_saas_list_plans`       | List SaaS plans          | `companyId`     |
| `ghl_saas_get_subscription` | Get subscription status  | —               |
| `ghl_saas_enable`           | Enable SaaS for location | `planId`        |

### AI Agents

Multi-step autonomous agents powered by LangGraph. Each agent fetches data from multiple GHL domains, uses an LLM to reason about the best action, and executes it. High-stakes write actions (send SMS/email, move pipeline stage, enroll workflow) pause for human approval before executing.

| Tool                         | Description                                                                                                                                               | Required Params         |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
| `ghl_agent_sales_automation` | Run a full sales automation sequence for a contact: fetch contact + opportunities + conversations → LLM analysis → execute action (or pause for approval) | `contact_id`            |
| `ghl_agent_approve_action`   | Approve or reject a high-stakes action proposed by a suspended agent run                                                                                  | `thread_id`, `approved` |

**How the HITL (Human-in-the-Loop) flow works:**

1. Call `ghl_agent_sales_automation(contact_id="abc123")` — agent fetches data and reasons over it
2. If the LLM proposes a high-stakes action (send SMS, move pipeline, etc.), it returns:
   ```JSON
   { "status": "awaiting_approval", "thread_id": "uuid", "proposed_action": {...}, "reasoning": "..." }
   ```
3. Review the proposed action, then call `ghl_agent_approve_action(thread_id="uuid", approved=true)` to execute it or `approved=false` to cancel
4. For low-stakes actions (add note, add tag), the agent executes immediately and returns `"status": "completed"`

To resume a previously suspended run without re-running data fetching, pass `thread_id` to `ghl_agent_sales_automation`.

**LLM setup:** Set `LLM_PROVIDER`, `ANTHROPIC_API_KEY` (or `OPENAI_API_KEY`), and `LLM_MODEL` in your `.env`. The agent layer is model-agnostic — swap providers without changing any agent code.

***

## Webhook Setup

1. In GHL: go to **Settings → Webhooks → Add Webhook**
2. Set the URL to `https://your-server.com/webhooks/ghl`
3. Copy the **Signing Secret** from GHL and set `GHL_WEBHOOK_SECRET=<secret>` in `.env`
4. Select the event types you want to receive

### Supported Events

| Event Type                | Triggered When            |
| ------------------------- | ------------------------- |
| `ContactCreate`           | New contact created       |
| `ContactUpdate`           | Contact fields changed    |
| `ContactDelete`           | Contact deleted           |
| `OpportunityCreate`       | New opportunity created   |
| `OpportunityUpdate`       | Opportunity updated       |
| `OpportunityStatusChange` | Opportunity won/lost/etc. |
| `InboundMessage`          | Contact sent a message    |
| `OutboundMessage`         | Message sent to contact   |
| `AppointmentCreate`       | Appointment booked        |
| `AppointmentUpdate`       | Appointment changed       |
| `NoteCreate`              | Note added to contact     |
| `TaskCreate`              | Task added to contact     |
| `FormSubmission`          | Form submitted            |
| `PaymentSuccess`          | Payment completed         |

### Custom Event Handlers

In `mcp/webhooks.py`, add your own logic:

```Python
from mcp.webhooks import webhook_handler

@webhook_handler("ContactCreate")
async def my_handler(event: dict) -> None:
    contact_id = event.get("id")
    # your custom logic here
```

***

## Docker

```Shell
# Build and start
docker-compose up -d

# View logs
docker-compose logs -f ghl-mcp-server

# Stop
docker-compose down
```

The `data/` directory is mounted as a volume to persist OAuth tokens across restarts.

***

## Running Tests

```Shell
# Install dev dependencies
pip install -e ".[dev]"

# Run all tests with coverage
pytest

# Run specific module
pytest tests/test_contacts.py -v

# Run with HTML coverage report
pytest --cov-report=html
# Open htmlcov/index.html in browser
```

***

## How to Add a New Tool

1. **Add the API method** in `api/<module>.py`:
   ```Python
   async def my_new_action(self, location_id: str, param: str) -> dict[str, Any]:
       return await self._client.post("/endpoint", location_id=location_id, json={"param": param})
   ```

2. **Add the Pydantic model** in `models/<module>.py` (if new response shape):
   ```Python
   class MyNewModel(BaseModel):
       id: str
       field: str
   ```

3. **Add the Tool definition** in `mcp/tools/<module>.py` — append to the `_<module>_tools()` list:
   ```Python
   Tool(
       name="ghl_module_my_new_action",
       description="Clear description of what this does and when to use it.",
       inputSchema={
           "type": "object",
           "properties": {
               "locationId": {"type": "string"},
               "param": {"type": "string", "description": "What this param does"},
           },
           "required": ["param"],
       },
   ),
   ```

4. **Add the dispatch case** in `_dispatch()` in the same file:
   ```Python
   elif name == "ghl_module_my_new_action":
       result = await api.my_new_action(location_id, arguments["param"])
   ```

5. **Write a test** in `tests/test_<module>.py`:
   ```Python
   @pytest.mark.asyncio
   async def test_my_new_action(module_api, mock_ghl):
       mock_ghl.post("/endpoint").mock(return_value=httpx.Response(200, json={"ok": True}))
       result = await module_api.my_new_action("loc_id", "value")
       assert result["ok"] is True
   ```

6. **Register the tool** — it's already wired up via `mcp/server.py` dispatch based on prefix.

***

## Rate Limits

GHL enforces:

* **Burst limit:** 100 requests per 10 seconds per location
* **Daily limit:** \~200,000 requests per day

This server handles both automatically:

* **Token bucket** per location: waits for refill if empty (never drops requests)
* **Daily warning:** logs a warning at 80% of daily limit
* **Retry on 429:** exponential backoff with jitter (up to 3 retries)

When the rate limit is hit, the tool call will **wait** (up to \~7 seconds across retries) rather than fail immediately.

***

## Troubleshooting

| Error                                     | Cause                                             | Fix                                                                     |
| ----------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------- |
| `AuthenticationError: token invalid`      | `GHL_PRIVATE_TOKEN` is wrong or expired           | Regenerate token in GHL Settings                                        |
| `ConfigurationError: locationId required` | No locationId passed or `GHL_LOCATION_ID` not set | Set `GHL_LOCATION_ID` in `.env` or pass `locationId` in every tool call |
| `NotFoundError`                           | Resource ID doesn't exist in this location        | Verify the ID belongs to the correct location                           |
| `ValidationError (422)`                   | Required GHL fields missing or wrong format       | Check GHL API docs for required fields                                  |
| `GHLServerError (500/502/503)`            | GHL API is down                                   | Server retries up to 3 times with backoff                               |
| OAuth: `No token found for location`      | OAuth flow not completed                          | Visit `/oauth/authorize` and complete authorization                     |
| OAuth: `Token refresh failed`             | Refresh token expired (>60 days unused)           | Re-authorize via `/oauth/authorize`                                     |
| Webhook: `401 Invalid signature`          | `GHL_WEBHOOK_SECRET` mismatch                     | Copy exact secret from GHL webhook settings                             |
| `ImportError: mcp not found`              | MCP SDK not installed                             | Run `pip install -e .`                                                  |
| Agent: `LLM_PROVIDER not set`            | AI agent tools called without LLM config          | Set `LLM_PROVIDER` + `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in `.env` |
| Agent: `No pending approval for thread`  | `ghl_agent_approve_action` called with stale ID   | Restart with a new `ghl_agent_sales_automation` call                    |
| Agent: `contact_id is required`          | Called `ghl_agent_sales_automation` without ID    | Pass a valid GHL `contact_id`                                           |

***

## Architecture

```
main.py               ← Entrypoint; starts stdio or HTTP transport
config.py             ← pydantic-settings; all env config
auth/
  private_token.py    ← Bearer token injection
  oauth.py            ← Auth code grant, token refresh, SQLite storage
api/
  client.py           ← httpx AsyncClient; retry, rate-limit, error mapping
  contacts.py         ← Raw GHL API calls (no business logic)
  ...
ghl_mcp/
  server.py           ← MCP Server; registers all 113 tools
  tools/
    contacts.py       ← Tool definitions + dispatch for contacts
    ...                 (17 domain tool files, all untouched by agent layer)
  agents/             ← LangGraph agent layer (NEW)
    base.py           ← GHLAgentContext, get_llm(), truncate_text()
    sales_automation.py ← LangGraph StateGraph: 7 nodes + HITL interrupt
    registry.py       ← MCP tool definitions + agent dispatch router
    checkpointer.py   ← MemorySaver singleton for HITL state persistence
    tools.py          ← LangChain StructuredTool wrappers (for future ReAct agents)
  webhooks.py         ← FastAPI router; signature validation + event routing
models/               ← Pydantic v2 models for GHL response shapes
tests/                ← pytest with respx mock for every module
```

**Request flow — GHL tool (HTTP mode):**

```
Claude → SSE /mcp/sse → MCP Server → tool dispatch → API module → GHLClient → GHL API v2
```

**Request flow — AI agent tool:**

```
Claude → SSE /mcp/sse → MCP Server → _agent_dispatch → LangGraph StateGraph
    → [fetch_contact] → [fetch_opportunities] → [fetch_conversations]
    → [analyze_and_plan (LLM)] → [request_approval (interrupt)] or [execute_action]
    → API module → GHLClient → GHL API v2
```

**Agent HITL (Human-in-the-Loop) checkpoint flow:**

```
ghl_agent_sales_automation  →  interrupt()  →  MemorySaver saves state
                                               → returns "awaiting_approval"
ghl_agent_approve_action    →  Command(resume=approved)  →  graph resumes
                                               → execute_action → completed
```