Skip to main content
Glama
alibhatti59

gohighlevel-mcp

by alibhatti59
README.md
# gohighlevel-mcp

An MCP (Model Context Protocol) server that exposes GoHighLevel CRM
operations as tools an LLM agent can call: search contacts, get a
contact record, list appointments, log a note, and create a new lead.

## Why this server

I maintain production GoHighLevel sub-accounts, Vapi voice agents, and
website chatbots for three real-estate brands as part of my day job.
The recurring pattern in that work is: a caller or website visitor
talks to an agent, the agent needs to check if they're already a CRM
contact, see their history and upcoming appointments, log what
happened, and either update or create their record. This server
exposes exactly that loop as MCP tools, so any MCP-compatible agent
(Claude, an n8n AI agent node, a custom LangChain agent, etc.) can plug
straight into a GoHighLevel sub-account.

## Design choices

- **Python + FastMCP** (`mcp.server.fastmcp`) — matches the rest of my
  stack (FastAPI, Python automation scripts) and gives automatic input
  validation via Pydantic without hand-rolled JSON schemas.
- **Mock data backend for this build.** I don't have a spare GHL
  sub-account API key to hand over for an evaluation task, so
  `data_store.py` reads/writes a local `mock_data.json` instead of
  calling `https://services.leadconnectorhq.com`. Every method in
  `GHLDataStore` is named and shaped after the real GHL v2 endpoint it
  stands in for (see the docstring at the top of `data_store.py`), so
  swapping in an `httpx.AsyncClient` with a real API key is a
  same-signature change, not a rewrite.
- **Tools, not raw endpoint wrappers.** `ghl_search_contacts` +
  `ghl_get_contact` + `ghl_list_appointments` + `ghl_create_note` +
  `ghl_create_contact` map onto real agent tasks (look someone up,
  check their history, log the outcome), not a 1:1 dump of every GHL
  API route.
- **Read-only tools are annotated `readOnlyHint: true`**; the two
  write tools (`ghl_create_note`, `ghl_create_contact`) are not, so a
  permission-gating layer upstream can treat them differently — which
  matters a lot once an agent is allowed to touch real CRM data.
- **Errors are actionable, not stack traces.** A lookup on an unknown
  `contact_id` returns `{"error": ..., "suggestion": "Use
  ghl_search_contacts to find the correct contact_id."}` instead of
  crashing the agent's turn.

## Project structure

```
gohighlevel-mcp/
├── server.py        # MCP server + tool definitions (FastMCP)
├── data_store.py     # Data access layer (mock JSON now, real GHL API later)
├── mock_data.json    # Sample CRM data: 3 contacts, 2 appointments
├── test_server.py    # End-to-end test: spins up server.py as a real
│                      # stdio subprocess and calls every tool through
│                      # a proper MCP ClientSession
└── requirements.txt
```

## Run it

```bash
pip install -r requirements.txt
python server.py                # stdio transport, for local MCP clients
python server.py --http         # streamable HTTP on :8000, for remote clients
```

## Test it

```bash
python test_server.py
```

This launches the server as a real subprocess over stdio, connects a
real `ClientSession`, calls `list_tools`, then exercises all 5 tools
(search → get → list appointments → add note → handle a bad id
gracefully → create a new lead) and asserts on the results. Not a
mocked client — this is the same protocol path a real agent uses.

## What I'd add next

- Swap `GHLDataStore` for an `httpx.AsyncClient`-backed version that
  calls the real GHL API with a Location API key from an env var.
- Pagination cursor support to match GHL's actual cursor-based
  pagination (this mock uses simple offset/limit for clarity).
- A `ghl_update_pipeline_stage` tool, since stage changes are the other
  half of the lead-routing loop I handle day to day.