Skip to main content
Glama
zackscriven

ghl-mcp-server-v2

by zackscriven
README.md
# ghl-mcp-server-v2 — The Definitive GoHighLevel MCP Server

A from-the-ground-up MCP server covering **the full public GoHighLevel API surface: 667 unique operations** enumerated from the complete official OpenAPI spec corpus (84 spec files, legacy + v3 tiers). **651 tools registered**; the other 16 are explicit, itemized exceptions (see `api-coverage-checklist.md`) — never silent omissions.

Built 2026-07-16 in a Cowork session (orchestrator + 8 parallel domain-audit sub-agents), following the `mcp-builder` skill's four-phase workflow. TypeScript, stdio transport, multi-tenant (`locationId`/`companyId` are call-time parameters — nothing tenant-specific is hardcoded).

## Why this exists

The previous production connector (`MCP_Servers/_to_delete/ghl-mcp-server-v1-retired`) was a thin wrapper with silently incomplete coverage. Confirmed failures, all fixed here and locked in as regression checks (verified per-domain in `docs/audits/`):

| Old connector failure | This server |
|---|---|
| `update_opportunity` dropped `assignedTo`, `pipelineId`, `forecastExpectedCloseDate`, `forecastProbability`, `customFields` | `ghl_opportunity_update` exposes all 9 documented body fields |
| `update_contact` dropped `assignedTo`, `customFields`, `tags`, `dnd`/`dndSettings`/`inboundDndSettings`, address fields, `dateOfBirth`, `source`, `website`, `timezone` | `ghl_contact_update` exposes all 20 documented body fields |
| `create_task` always returned 422 | Root-caused: the API requires `title`, `dueDate` AND `completed` on create; the schema now enforces exactly that (and exposes `assignedTo`) |
| `list_opportunities` could never page past record 100 (ignored `startAfter`/`startAfterId`/`page`) | `ghl_opportunity_search` exposes all ~20 filter/cursor params; cursors pass through verbatim |
| `list_media` 422'd (no way to pass required `type`) | `ghl_media_list_files` requires `type`, `altType`, `altId`, `sortBy`, `sortOrder` per spec |
| Single hardcoded `Version` header | Version header resolved **per operation** from the spec that documents it (494 tools send `v3`, 117 send `2021-07-28`, 10 send `2021-04-15`, 30 send none — e.g. store ops) |
| Social `create_post` regressed | Schema verified faithful to spec; tool description carries a warning that the live endpoint had a server-side regression as of 2026-07-04 |
| `ghl_conversation_send_message` 422'd on every send (fix 2026-08-26) | Upstream spec bug, not extraction loss: `subType` emitted as an empty `object` schema (live API validates a string enum — `{}` returned `422 CONVERSATIONS_VALIDATION_ERROR`) and `subType`/`status` wrongly listed in `required`. Fixed via the new `patchBody` override layer (`src/overrides/conversations.ts`): both now optional strings, omitted from the outbound body when absent; required is `[type, contactId]`. Resolves the 2026-08-26 send failure. |
| 12 more fields drive-wide emitted as empty `object` schemas (fix 2026-08-27) | Same upstream spec-bug pattern, swept and patched via `patchBody`: contacts `dateOfBirth` (create/update/upsert), associations `firstObjectLabel`/`firstObjectKey`/`secondObjectLabel`/`secondObjectKey` (create/update — these were *required*, so both tools were uncallable), `ghl_opportunity_upsert` `monetaryValue` (→ number), `ghl_phone_purchase_number` `numberType` (required — uncallable), `ghl_product_review_bulk_update` `status` (required — uncallable), social `postType` (→ enum post/story/reel), queue `status`, facebook account `type`. Registry-level regression scan now reports zero empty-object-with-scalar-example fields across all 651 tools. |

## Architecture — how "no silently dropped field" is guaranteed

Tools are **generated from the official OpenAPI specs**, not hand-transcribed:

1. `scripts/extract-operations.mjs` parses every spec file, fully dereferences `$ref`s, dedupes by `METHOD /path` (v3 spec variant preferred; legacy-only operations keep their legacy `Version` header), and emits `src/generated/operations.json` with request schemas **verbatim**.
2. `src/registry.ts` builds one MCP tool per operation: path/query/header params become top-level arguments; the request body schema is carried verbatim under a `body` argument. Missing path-param declarations in the spec are inferred from the URL template.
3. `src/overrides/*.ts` — eight domain-audit modules (one per sub-agent) carrying curated tool names, Version-header corrections, content-type overrides (multipart, form-urlencoded), body-schema patches for known-wrong specs (`patchBody`, added 2026-08-26 — see `ghl_conversation_send_message`), agency-token warnings, and the 16 skip-with-reason exceptions.

Because the schema pipeline is mechanical, a field can only be missing if it is missing from GoHighLevel's own published spec — and the three known cases of that are documented in the audits (`POST /contacts/search`, object-record create/update, `POST /forms/upload-custom-files`), each exposed with a permissive body rather than dropped.

## Setup

```bash
npm install
npm run build        # tsc + copies src/generated into build/
npm start            # stdio server
npm run inspect      # MCP Inspector smoke test
```

Client config (e.g. Claude Desktop / Cowork):

```json
{
  "mcpServers": {
    "ghl-v2": {
      "command": "node",
      "args": ["<path>/ghl-mcp-server/build/index.js"],
      "env": { "GHL_API_KEY": "...", "GHL_ENABLE_WRITES": "false" }
    }
  }
}
```

## Environment variables

| Var | Default | Meaning |
|---|---|---|
| `GHL_API_KEY` (fallback `HD_GHL_API_KEY`) | — | Private-integration token / OAuth access token. Env-only, never hardcoded. |
| `GHL_ENABLE_WRITES` (fallback `HD_GHL_ENABLE_WRITES`) | **unset = read-only** | The dry-run safety gate. Unless set to `true`/`1`, every non-GET call is blocked with an explanatory error (arguments are still validated, so you can dry-run writes). Default-safe because this can point at a live healthcare-business CRM. |
| `GHL_ENABLED_DOMAINS` | all | Comma-separated spec basenames (e.g. `contacts,opportunities,calendars`) to expose a subset of the 651 tools — useful to keep client context small. |
| `GHL_BASE_URL` | `https://services.leadconnectorhq.com` | Override for testing. |
| `GHL_TIMEOUT_MS` | `30000` | Request timeout. |
| `GHL_SPECS_DIR` | (PPC corpus path) | Only used by `npm run extract` to regenerate `operations.json` when GHL ships spec updates. |

## Conventions

- Tool names: `ghl_<resource>_<action>` (snake_case, ≤64 chars), curated per domain (e.g. `ghl_contact_update`, `ghl_opportunity_search`, `ghl_calendar_appointment_create`, `ghl_invoice_schedule_cancel`).
- Annotations: `readOnlyHint` (GET), `destructiveHint` (DELETE), `idempotentHint` (GET/PUT/DELETE), `openWorldHint` always true.
- Multi-tenant: pass `locationId` / `companyId` / `altId` per call. ~44 platform ops require an **agency-level (Company) token** — their descriptions say so.
- Multipart uploads: pass file fields as `{ "filePath": "...", "filename?": "...", "mimeType?": "..." }` inside `body`; the server builds the multipart request.
- Responses: `structuredContent = { status, ok, data }` plus pretty-printed text; errors return real HTTP status + body + actionable hints.

## Deliverables map

- `api-coverage-checklist.md` — all 667 operations, per-row status, exceptions itemized. **This is the proof of coverage.**
- `docs/audits/*.md` — the 8 domain audit reports (regression verifications, renames, spec bugs found).
- `docs/evaluation.xml` — 10 verified evaluation Q&A pairs (mcp-builder Phase 4 format).
- `docs/generated-names.json` — `"METHOD /path"` → tool-name map.

## Not done here (operator steps)

- **Live testing.** This was a docs-driven build; no live GHL call was made. Smoke-test read-only against a real location, then enable writes deliberately.
- Registering the server in a live MCP config (kept separate from the existing `ghl-mcp-server` registration — do not overwrite it without deciding to retire the old one).
- Re-run `npm run extract && npm run build` when GHL ships API changes; the checklist regenerates from the same pipeline.

TDQS

C2.9/5.0

Scored across 651 tools

Disambiguation2/5

The descriptions are individually thorough and explicitly call out deprecated/legacy variants, but the sheer volume of near-duplicate pairs makes selection genuinely ambiguous: legacy and v3 twins (e.g. ghl_saas_bulk_disable vs ghl_saas_bulk_disable_deprecated, ghl_email_builder_template_* vs ghl_email_template_*), two opportunity search tools, multiple overlapping 'get Facebook pages' tools across ghl_social_* and ghl_ad_fb_*, and transcription get vs download. Even with careful prose, an agent facing 651 tools will struggle to pick the right one.

Naming Consistency4/5

The ghl_<domain>_<resource>_<action> pattern is largely consistent, with sensible domain prefixes (contact, calendar, ad_fb, voice_ai) and helpful suffixes like _deprecated and _legacy. Minor deviations exist: verb-first names (ghl_social_get_posts, ghl_blog_get_all_authors_by_location), mixed edit/update verbs (ghl_social_edit_post vs ghl_social_update_queue_item), and ghl_marketplace_charge which reads ambiguously as verb or noun, but overall the system is quite disciplined.

Tool Count1/5

651 tools is an extreme mismatch for a single MCP server, far beyond the 25+ threshold. While it reflects the breadth of the full GoHighLevel platform, no agent can sanely navigate this surface in one server — it should be split into domain-focused servers (contacts, calendars, ad publishing, payments, SaaS, etc.) to keep tool selection tractable and context overhead manageable.

Completeness5/5

The surface provides full CRUD and lifecycle coverage across dozens of domains: contacts, calendars, opportunities, invoices, payments, products, emails, social posting, ad publishing, voice/conversation AI, knowledge bases, SaaS, and more. The few known gaps (e.g. ghl_user_delete/update missing a path param, form file upload with no callable schema) are documented tool bugs rather than missing operations; there are no obvious dead ends in the core workflows.

Maintenance

ActivityMaintained
ResponsivenessNo issues