Skip to main content
Glama
README.md
# GoHighLevel MCP Server

A Model Context Protocol server that gives an LLM agent operational control of a
[GoHighLevel](https://www.gohighlevel.com) CRM — 114 tools across 24 modules,
covering contacts, pipelines, calendars, messaging, invoicing, and payments over
the GoHighLevel API v2.

## The problem

GoHighLevel is the system of record for a small agency: every client, every
booking, every invoice. The work that actually eats the day is not any single
CRM action but the stitching between them — a shoot gets confirmed, so someone
has to create the opportunity, move it to the right pipeline stage, book the
calendar slot against the right contact, draft the invoice, and log a note. Each
step is thirty seconds of clicking, and the sequence runs several times a week.

That sequence is exactly what an agent can do, if it can reach the CRM. This
server is that reach: it exposes GoHighLevel as typed, annotated tools so an
agent can carry out the whole chain from a sentence of instruction, while the
destructive and outward-facing steps stay visible for approval.

## Architecture

24 tool modules register against one `McpServer` over stdio. Everything routes
through a single `ghlRequest()` that owns auth, the mandatory `Version` header,
query-string assembly, and error shaping. Modules are toggleable at startup via
`GHL_DISABLED_MODULES` — which matters more than it sounds, because 114 tool
definitions is a meaningful chunk of an agent's context window before it has
read a single word of the user's request. A deployment that only does bookings
can register six modules and skip the rest.

```
  MCP host (Claude Desktop / Claude Code)
          | stdio (JSON-RPC)
  +-------v--------------------------------------------+
  |  index.ts   MODULES registry, GHL_DISABLED_MODULES  |
  +-------+--------------------------------------------+
          |
  +-------v-----+ +---------------+ +-----------+ ......  24 modules
  |  contacts   | | opportunities | | invoices  |
  +-------+-----+ +-------+-------+ +-----+-----+
          |               |               |
          |               |         +-----v--------------+
          |               |         | billing-helpers.ts |
          |               |         |  businessDetails   |
          |               |         |  contactDetails    |
          |               |         |  sender resolution |
          |               |         +-----+--------------+
          +-------+-------+---------------+
                  |
        +---------v----------------------------+
        |  client.ts  ghlRequest()             |
        |   Bearer token + Version header      |
        |   status-specific error hints        |
        +---------+----------------------------+
                  |
          services.leadconnectorhq.com
```

Every write tool carries MCP annotations — 17 are marked `destructiveHint`, and
`ghl_send_message` / `ghl_send_invoice` are flagged as outward-facing because
they contact real customers. The host surfaces those before approving a call,
which is the difference between an agent that drafts an invoice and an agent
that mails one to a client by accident.

## The genuinely hard part

Creating an invoice. The endpoint takes `businessDetails` and `contactDetails`
blocks, and the documentation understates both: pass a `contactId` and a couple
of line items, as the docs suggest, and you get a validation error that names no
field. Both blocks are required in full, and `businessDetails.phoneNo` and
`contactDetails.phoneNo` are mandatory — a contact with an email and no phone
cannot be invoiced at all.

Worse, the values have to *match what the UI produces*, or invoices created via
the API look different from invoices created by hand — different logo, missing
terms, wrong numbering. Those defaults are not in the location profile where
you would expect them; they live behind `GET /invoices/settings`, which is the
same source the UI pre-fills from.

[src/tools/billing-helpers.ts](src/tools/billing-helpers.ts) resolves both
blocks so the tools only need a `contactId`. Business details fall through four
levels — per-call argument, `GHL_BUSINESS_*` env, saved invoice settings,
location profile — with each level filling only what the one above left blank.
Contact details are fetched and assembled, with `name` falling back through
full name, first+last, company name, email, then phone, because GoHighLevel
rejects an empty name and real CRM records are frequently missing one. Both
paths throw a message naming the missing field and how to supply it, rather
than surfacing GHL's opaque 422. Each lookup is memoised per location so a batch
of ten invoices costs one settings fetch, not ten.

## What I'd do differently

1. **No tests.** 4,000 lines and none. The fallback chain in `billing-helpers`
   is pure logic over fixture data — the easiest thing in the repo to test and
   the most costly to get wrong, since the failure mode is a malformed invoice
   sent to a client.
2. **No retry on 429.** `ghlRequest` tells the caller "rate limited; retry after
   a short delay" and then does not retry. Backoff belongs in the client, not in
   the agent's judgement.
3. **The caches are module-level mutable maps with no invalidation.** Correct
   for a stdio server the host restarts freely; wrong the moment this runs as a
   long-lived process, where a business-profile edit would never be picked up.
4. **Responses are `Record<string, unknown>` throughout.** GoHighLevel publishes
   an OpenAPI spec; generating types from it would turn a class of runtime
   surprises into compile errors.
5. **114 tools in one server is too many.** Module toggles are a workaround, not
   a fix. The better shape is a small set of tools plus a discovery mechanism,
   so the agent pays for what it uses.

## Setup

Requires Node 20+ and a GoHighLevel account.

### 1. Create a Private Integration token

**Settings → Private Integrations → Create new integration.** Enable the scopes
matching the tools you intend to use; at minimum:

`contacts.readonly`, `contacts.write`, `opportunities.readonly`,
`opportunities.write`, `calendars.readonly`, `calendars/events.write`,
`conversations.readonly`, `conversations/message.write`, `invoices.readonly`,
`invoices.write`, `products.readonly`, `products.write`,
`locations/customFields.readonly`, `workflows.readonly`

Copy the token — it starts with `pit-`.

### 2. Find your Location ID

**Settings → Business Profile**, or read it out of the dashboard URL:
`.../location/<LOCATION_ID>/...`

### 3. Build

```bash
git clone <this-repo>
cd ghl-mcp
npm install
npm run build
```

### 4. Register with an MCP host

```json
{
  "mcpServers": {
    "gohighlevel": {
      "command": "node",
      "args": ["/absolute/path/to/ghl-mcp/dist/index.js"],
      "env": {
        "GHL_API_KEY": "pit-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "GHL_LOCATION_ID": "your-location-id"
      }
    }
  }
}
```

Restart the host. See [.env.example](.env.example) for every supported variable,
including the invoice business block and the module toggles.

To exercise the server without a host:

```bash
GHL_API_KEY=pit-... GHL_LOCATION_ID=... npm run inspect
```

### A note on "building automations"

GoHighLevel's API cannot create workflow logic — the visual builder is UI-only.
The supported pattern is to build the workflow once in the UI, find its id with
`ghl_list_workflows`, and enrol contacts with `ghl_add_contact_to_workflow`.

## Tool reference

| Area | Tools |
| --- | --- |
| **Contacts** | `ghl_search_contacts`, `ghl_get_contact`, `ghl_create_contact`, `ghl_update_contact`, `ghl_add_contact_tags`, `ghl_delete_contact` |
| **Opportunities / Pipelines** | `ghl_get_pipelines`, `ghl_search_opportunities`, `ghl_get_opportunity`, `ghl_create_opportunity`, `ghl_update_opportunity` |
| **Calendars / Appointments** | `ghl_get_calendars`, `ghl_get_free_slots`, `ghl_create_appointment`, `ghl_get_appointment`, `ghl_update_appointment`, `ghl_delete_appointment` |
| **Conversations / Messaging** | `ghl_search_conversations`, `ghl_get_messages`, `ghl_send_message` |
| **Invoices** | `ghl_list_invoices`, `ghl_get_invoice`, `ghl_create_invoice`, `ghl_send_invoice`, `ghl_void_invoice`, `ghl_delete_invoice` |
| **Estimates** | `ghl_list_estimates`, `ghl_generate_estimate_number`, `ghl_create_estimate`, `ghl_update_estimate`, `ghl_send_estimate`, `ghl_estimate_to_invoice`, `ghl_delete_estimate` |
| **Products** | `ghl_list_products`, `ghl_get_product`, `ghl_create_product`, `ghl_update_product`, `ghl_delete_product`, `ghl_list_product_prices`, `ghl_create_product_price` |
| **Custom Fields** | `ghl_list_custom_fields`, `ghl_get_custom_field`, `ghl_create_custom_field`, `ghl_update_custom_field`, `ghl_delete_custom_field` |
| **Tasks** | `ghl_list_contact_tasks`, `ghl_get_contact_task`, `ghl_create_contact_task`, `ghl_update_contact_task`, `ghl_delete_contact_task` |
| **Notes** | `ghl_list_contact_notes`, `ghl_get_contact_note`, `ghl_create_contact_note`, `ghl_update_contact_note`, `ghl_delete_contact_note` |
| **Workflows (automations)** | `ghl_list_workflows`, `ghl_add_contact_to_workflow`, `ghl_remove_contact_from_workflow` |
| **Payments** | `ghl_list_orders`, `ghl_get_order`, `ghl_list_transactions`, `ghl_list_subscriptions`, `ghl_get_subscription` |
| **Forms & Surveys** | `ghl_list_forms`, `ghl_get_form_submissions`, `ghl_list_surveys`, `ghl_get_survey_submissions` |
| **Users & Teams** | `ghl_list_users`, `ghl_get_user` |
| **Calendar events** | `ghl_get_calendar_events`, `ghl_block_calendar_slot`, `ghl_list_appointment_notes`, `ghl_create_appointment_note` |
| **Social Planner** | `ghl_list_social_accounts`, `ghl_list_social_posts`, `ghl_get_social_post`, `ghl_create_social_post`, `ghl_delete_social_post` |
| **Media Library** | `ghl_list_media`, `ghl_upload_media_by_url`, `ghl_delete_media` |
| **Campaigns & Links** | `ghl_list_campaigns`, `ghl_add_contact_to_campaign`, `ghl_remove_contact_from_campaign`, `ghl_list_trigger_links`, `ghl_create_trigger_link`, `ghl_delete_trigger_link` |
| **Tags** | `ghl_list_tags`, `ghl_create_tag`, `ghl_update_tag`, `ghl_delete_tag` |
| **Custom Values** | `ghl_list_custom_values`, `ghl_get_custom_value`, `ghl_create_custom_value`, `ghl_update_custom_value`, `ghl_delete_custom_value` |
| **Businesses** | `ghl_list_businesses`, `ghl_get_business`, `ghl_create_business`, `ghl_update_business`, `ghl_delete_business` |
| **Custom Objects** | `ghl_list_object_schemas`, `ghl_get_object_schema`, `ghl_search_object_records`, `ghl_get_object_record`, `ghl_create_object_record`, `ghl_update_object_record`, `ghl_delete_object_record` |
| **Associations** | `ghl_list_associations`, `ghl_get_record_relations`, `ghl_create_relation`, `ghl_delete_relation` |
| **Funnels** | `ghl_list_funnels`, `ghl_list_funnel_pages` |

## Licence

MIT — see [LICENSE](LICENSE). Not affiliated with or endorsed by GoHighLevel.

TDQS

B3.1/5.0

Scored across 114 tools

Disambiguation4/5

Tool names consistently include the target resource (contact, opportunity, invoice, task, note, etc.), so most actions are clearly distinct even with 114 tools. A few near-neighbor pairs—custom_field vs custom_value, contact_note vs appointment_note, and association vs relation—could require careful description reading, but they are not genuinely interchangeable.

Naming Consistency4/5

The dominant ghl_<verb>_<noun> pattern is clear and mostly followed. However, several plural list operations use get_ instead of list_ (ghl_get_calendars, ghl_get_pipelines, ghl_get_messages, ghl_get_form_submissions), and ghl_estimate_to_invoice breaks the verb_noun pattern.

Tool Count1/5

114 tools is an extreme count for an agent-facing MCP server; even though GoHighLevel covers many domains, this surface is far beyond the 3-15 tool range considered well-scoped. The sheer number will overwhelm tool selection and context windows regardless of how individually useful the tools are.

Completeness3/5

The server covers a very wide range of GHL resources with strong CRUD coverage for contacts, tasks, notes, products, tags, custom values, and businesses. Notable gaps exist, though: there is no ghl_get_estimate (making estimate updates awkward), no ghl_update_invoice, and no delete for opportunities, leaving some lifecycle dead ends.

Maintenance

ActivityMaintained
ResponsivenessNo issues