@theethosteam/ghl-mcp
v1.0.0
Published
Agency-wide read + write MCP server (and CLI) for GoHighLevel / LeadConnector. ~69 typed tools (contacts, forms & submissions, conversations, opportunities, calendars/appointments, users, tags, custom fields, workflows, products, invoices/payments) + a se
Maintainers
Readme
@theethosteam/ghl-mcp
Rock-solid, agency-wide MCP server + CLI for GoHighLevel / LeadConnector. ~69 typed tools across the hot domains, a searchable catalog of all 576 v2 endpoints (ghl_catalog), and a ghl_raw passthrough to anything — so every endpoint is reachable. Runs via npx, identically locally or in a cloud session.
Built on the same pattern as the GA4 / GTM / GSC / Google Ads MCPs: one shared core/, exposed as both an MCP server (mcp.ts) and a commander CLI (cli.ts), run directly with tsx (no build step).
What makes it agency-grade
- Agency OAuth, durable across cloud restarts. One private Marketplace app installed at the agency mints a short-lived token for any sub-account on demand. GHL rotates the refresh token on every refresh (single-use), so the live token lives in a durable token store (Supabase when configured) with compare-and-swap — local and ephemeral cloud share one source of truth and concurrent cold-starts never strand a token. A per-location PIT still works as an override.
- Account-awareness is structural. Every call binds to exactly one sub-account and every response echoes
{ locationId, locationName }. You always know which account you touched. - Tiered write-safety. Routine single-record writes (create/update contact, tags, notes, tasks, send a requested message, move a stage, book an appointment) execute directly. Destructive (deletes), bulk, and money (invoices/payments) ops are confirm-gated dry-runs — GHL has no server-side validate, so without
confirm:truethey don't call the API; they return a preview naming the target account. Cross-account writes are refused. - Hardened transport. Retry on 429 / 5xx / network with exponential backoff + jitter, per-call timeout, structured
GhlError, defensive auto-pagination, response trimming.
Auth
(A) Agency OAuth — reach every sub-account (recommended)
npx -y @theethosteam/ghl-mcp ghl auth:scopes # the full scope set to select on the app
npx -y @theethosteam/ghl-mcp ghl auth:url --client-id <id> --redirect <uri> # open, approve, copy ?code=…
npx -y @theethosteam/ghl-mcp ghl auth:exchange <code> --redirect <uri> # prints refresh_token + companyId- Create a Private Marketplace app at https://marketplace.gohighlevel.com (private = no review; 5-agency-install cap is fine for one agency).
- Select the scopes from
auth:scopes, set the redirect URI, generate the Client ID/Secret, and install at the agency (Company) level. - Run
auth:url→ approve →auth:exchange <code>to mint the seed refresh token. - Configure env + seed the token store:
ZJ_GHL_OAUTH_CLIENT_ID=...
ZJ_GHL_OAUTH_CLIENT_SECRET=...
ZJ_GHL_OAUTH_REFRESH_TOKEN=... # seed; the live token then lives in the store
ZJ_GHL_COMPANY_ID=...
ZJ_SUPABASE_URL=... # durable token store (see Token store below)
ZJ_GHL_SUPABASE_SERVICE_KEY=... # service-role key(B) Per-location PIT — immediate, single sub-account
ZJ_GHL_PIT=pit-xxxxxxxx
ZJ_GHL_LOCATION=<locationId> # multi-account: ZJ_GHL_PIT_<locationId>=pit-...A configured PIT wins; otherwise the connector mints a location token via OAuth.
Token store (durable OAuth)
Apply sql/ghl_oauth_token_store.sql once to the Supabase project named by ZJ_SUPABASE_URL — a tiny table + two security definer RPCs (ghl_token_get / ghl_token_rotate), service-role only. The connector reads/CAS-writes the rotating refresh token there so cloud + local stay in sync. Without it, the connector falls back to ZJ_GHL_OAUTH_REFRESH_TOKEN in env (fine for local; not durable in ephemeral cloud).
Optional: ZJ_GHL_API_VERSION (default 2021-07-28), ZJ_GHL_BASE_URL, ZJ_GHL_TIMEOUT_MS, ZJ_GHL_MAX_PAGES, ZJ_GHL_MAX_RESPONSE_ITEMS.
Connect to Claude
Cloud (Claude Code on the web) — add to the repo .mcp.json (env via ${VAR}; secrets live in the environment box, never in git):
{
"mcpServers": {
"ghl": {
"command": "npx",
"args": ["-y", "@theethosteam/ghl-mcp"],
"env": {
"ZJ_GHL_OAUTH_CLIENT_ID": "${ZJ_GHL_OAUTH_CLIENT_ID}",
"ZJ_GHL_OAUTH_CLIENT_SECRET": "${ZJ_GHL_OAUTH_CLIENT_SECRET}",
"ZJ_GHL_OAUTH_REFRESH_TOKEN": "${ZJ_GHL_OAUTH_REFRESH_TOKEN}",
"ZJ_GHL_COMPANY_ID": "${ZJ_GHL_COMPANY_ID}",
"ZJ_SUPABASE_URL": "${ZJ_SUPABASE_URL}",
"ZJ_GHL_SUPABASE_SERVICE_KEY": "${ZJ_GHL_SUPABASE_SERVICE_KEY}"
}
}
}
}Pass each client's sub-account locationId per call (don't pin ZJ_GHL_LOCATION in a multi-client repo). For a single-location PIT setup, swap the OAuth env for ZJ_GHL_PIT + ZJ_GHL_LOCATION.
Local (Claude Code CLI):
claude mcp add ghl --scope user \
-e ZJ_GHL_OAUTH_CLIENT_ID=... -e ZJ_GHL_OAUTH_CLIENT_SECRET=... \
-e ZJ_GHL_OAUTH_REFRESH_TOKEN=... -e ZJ_GHL_COMPANY_ID=... \
-e ZJ_SUPABASE_URL=... -e ZJ_GHL_SUPABASE_SERVICE_KEY=... \
-- npx -y @theethosteam/ghl-mcpTools (~69 typed + catalog + raw)
- Awareness:
ghl_whoami·ghl_list_locations·ghl_get_location·ghl_use_location·ghl_compare - Contacts: list · search · get · create · update · upsert · delete · add/remove tags · notes (list/add) · tasks (list/add/complete) · appointments
- Conversations: search · get · create · messages · send · update status
- Opportunities: search · get · create · upsert · delete · pipelines · update · status
- Calendars / appointments: calendars CRUD · groups · free-slots · events · appointment get/create/update/delete
- Forms:
ghl_list_forms·ghl_list_form_submissions - Users · tags · custom fields/values · workflows · campaigns · products
- Invoices & payments (money tier): list/get invoices · create/send/record-payment (gated) · transactions · orders
ghl_catalog— search all 576 v2 endpoints (method/path/scopes/required params) → feed toghl_rawghl_raw— call ANY v2 endpoint (GET runs; non-GET confirm-gated;companyAuth:truefor agency-level)
Every gated write is a DRY-RUN unless confirm:true.
CLI (handy for testing)
npx -y @theethosteam/ghl-mcp ghl whoami
npx -y @theethosteam/ghl-mcp ghl locations # agency OAuth
npx -y @theethosteam/ghl-mcp ghl form-submissions -l <locationId> --formId <id>
npx -y @theethosteam/ghl-mcp ghl catalog "refund invoice"
npx -y @theethosteam/ghl-mcp ghl raw GET /contacts/search -l <locationId>License
MIT
