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
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues