@elitedcs/ghl-mcp 3.5.1 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +11 -10
- package/dist/index.js +188 -50
- package/package.json +2 -2
- package/templates/action-schemas.json +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.7.0 — Funnel-builder fixes (Don Harris audit)
|
|
4
|
+
|
|
5
|
+
**178 tools across 38 modules. Bundle: 288.4 KB.**
|
|
6
|
+
|
|
7
|
+
Implements all 7 fixes from Don Harris's 2026-05-14 audit. Don captured the correct endpoints via Chrome DevTools against `page-builder.leadconnectorhq.com` and `app.gohighlevel.com`, then verified each fix with curl round-trips against two live sub-accounts. Full credit to Don for the diagnostic work.
|
|
8
|
+
|
|
9
|
+
### The root cause
|
|
10
|
+
All funnel write tools were hitting endpoints that don't exist on `backend.leadconnectorhq.com/funnels/...`. The real save paths live on the same host but with different verbs, methods, and body shapes. Plus, mutating endpoints require `Origin` + `Referer` headers that `funnelRequest()` wasn't sending, causing 401 `{"message":"Error calling IAM service"}` on every write.
|
|
11
|
+
|
|
12
|
+
### Header fix (unblocks every funnel write)
|
|
13
|
+
Added `Origin: https://app.gohighlevel.com` and `Referer: https://app.gohighlevel.com/` to `funnelRequest()`. Without these, GHL's IAM rejects writes regardless of bearer token validity.
|
|
14
|
+
|
|
15
|
+
### Endpoint fixes
|
|
16
|
+
| Tool | Was | Now |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `update_page_content` | `PUT /page/{pageId}` | `POST /builder/prebuilt-section/sync/changes` with `{ locationId, pageId, pageData, write: true, isPublished }` |
|
|
19
|
+
| `update_funnel` | `PUT /funnel/{funnelId}` | `POST /funnel/update-settings` with full settings body. Rewritten with fetch-then-merge so you can change one field without blowing away the others. |
|
|
20
|
+
| `create_funnel_page` | `POST /page` | `POST /funnel/create-step` with client-generated step UUID + nested `step` object. Now also returns the generated `stepId` so the caller can use it with `update_funnel_step` / `delete_funnel_page` without a separate lookup. |
|
|
21
|
+
| `delete_funnel_page` | `DELETE /page/{pageId}` | `POST /funnel/delete-step` with `{ funnelId, stepId }`. **SIGNATURE CHANGED**: now takes `funnelId + stepId` instead of `pageId`. |
|
|
22
|
+
| `delete_funnel` | `DELETE /funnel/{id}` | `POST /funnel/delete` with `{ funnelId, locationId, userId }` |
|
|
23
|
+
|
|
24
|
+
### `update_funnel_step` (NEW)
|
|
25
|
+
Per-step renames, URL slugs, and domain attachments. `PUT /funnel/step/{funnelId}` with `{ stepId, name, url, domainName }`. Use this when you want to rename a single page entry inside a funnel without touching funnel-level settings.
|
|
26
|
+
|
|
27
|
+
### `WorkflowBuilderClient.getUserId()` (NEW, internal)
|
|
28
|
+
Public getter on the builder client. Required because `delete_funnel` needs `userId` in the request body. Not exposed via MCP.
|
|
29
|
+
|
|
30
|
+
### Round-trip verified in MCP Testing
|
|
31
|
+
- 6/6 tested funnel writes return success against real GHL backend (the 7th, `update_page_content`, was already verified by Don's 112KB real-page round-trip in his audit email).
|
|
32
|
+
- Created → renamed → added step → renamed step → deleted step → deleted funnel, all in one test pass.
|
|
33
|
+
|
|
34
|
+
### Honest scoping
|
|
35
|
+
- The 168/178 "without Firebase / with Firebase" split documented in `setup_ghl_mcp` and `enable_workflow_builder` is carried forward from v3.5.1 and hasn't been audited empirically. It's likely off in absolute terms (funnel/form/pipeline builders all gate on Firebase too). TODO to do a real audit.
|
|
36
|
+
- The bonus `update_workflow_actions` / `remove_from_workflow` read/write asymmetry Don flagged at the end of his email is not addressed in this release — separate concern, separate fix when prioritized.
|
|
37
|
+
|
|
38
|
+
### Files changed
|
|
39
|
+
- `src/tools/funnel-builder.ts` — full rewrite of all 6 broken endpoints + `update_funnel_step` added
|
|
40
|
+
- `src/workflow-builder-client.ts` — `getUserId()` public getter
|
|
41
|
+
- `src/setup-tool.ts` — tool counts refreshed
|
|
42
|
+
|
|
43
|
+
## 3.6.0 — `build_goal_event` (closes Don-e's last open complaint)
|
|
44
|
+
|
|
45
|
+
**177 tools across 38 modules. Bundle: 281.9 KB.**
|
|
46
|
+
|
|
47
|
+
Goal-event support, end-to-end. Don-e flagged on May 4 that the MCP couldn't read or edit goal events. v3.1.0's permissive schema fixed the read side. v3.6.0 fixes the build side.
|
|
48
|
+
|
|
49
|
+
### `build_goal_event` tool (NEW)
|
|
50
|
+
A high-level helper that emits a correctly-shaped `workflow_goal` node — same pattern as `build_if_else_branch`. Takes a goal condition, optional extras, and an action mode (exit / continue / goto). Returns a single node JSON ready to insert into your actions array.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
```
|
|
54
|
+
build_goal_event(
|
|
55
|
+
goal_condition: "review_request_clicked",
|
|
56
|
+
extras: { reviewTypes: ["sms", "email"], reviewLinkId: "" },
|
|
57
|
+
action: "exit",
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Goal events explained
|
|
62
|
+
Goal events sit inline in the workflow's action chain — they're NOT branching nodes. The previous action's `next` points to the goal node, and the goal node itself has no `next`. When the goal condition fires during workflow execution (e.g., a contact clicks a review request link), the configured action runs:
|
|
63
|
+
- `exit` — terminates this workflow path (verified)
|
|
64
|
+
- `continue` — passes through to a downstream node (schema accepts but not yet captured from UI)
|
|
65
|
+
- `goto` — jumps to a specific action node (requires `target_node_id`)
|
|
66
|
+
|
|
67
|
+
### Verified end-to-end
|
|
68
|
+
- Built a goal-event node via `build_goal_event` matching Jerry's UI-built sample. **13 of 13 structural checks passed.**
|
|
69
|
+
- Saved it to a fresh workflow in MCP Testing, read back via `get_workflow_full`. **5 of 5 persistence checks passed** — GHL accepted the shape and round-tripped it cleanly.
|
|
70
|
+
- Test workflow cleaned up via `delete_workflow_full`.
|
|
71
|
+
|
|
72
|
+
### Schema updates
|
|
73
|
+
- `src/workflow-action-types.ts` — added `WorkflowGoalAttributes` interface + `workflow_goal` variant in the discriminated union (was previously caught by the catch-all fallback)
|
|
74
|
+
- `templates/action-schemas.json` — added a `workflow_goal` entry with the canonical shape, notes, and a pointer to the builder
|
|
75
|
+
- `update_workflow_actions` description updated to mention `workflow_goal` + `build_goal_event`
|
|
76
|
+
|
|
77
|
+
### Honest scoping
|
|
78
|
+
Only one `goal_condition` value is currently verified end-to-end: `review_request_clicked`. GHL's UI exposes others (tag-based, appointment-based, etc.) — those pass through the builder via the permissive `goal_condition: string` parameter but haven't been captured as specific cases. A future patch can add named helpers / enum values per goal_condition type when more samples are captured.
|
|
79
|
+
|
|
80
|
+
### Files changed
|
|
81
|
+
- `src/tools/workflow-builder.ts` — `build_goal_event` tool; `update_workflow_actions` description refresh
|
|
82
|
+
- `src/workflow-action-types.ts` — new `WorkflowGoalAttributes` + union variant
|
|
83
|
+
- `templates/action-schemas.json` — `workflow_goal` reference entry
|
|
84
|
+
|
|
3
85
|
## 3.5.1 — doc-string accuracy
|
|
4
86
|
|
|
5
87
|
**176 tools across 38 modules. Bundle: 278.7 KB.**
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GHL Command — GoHighLevel MCP Server
|
|
2
2
|
|
|
3
|
-
**Full GoHighLevel API access for Claude.**
|
|
3
|
+
**Full GoHighLevel API access for Claude.** 178 tools across 36 modules — manage contacts, conversations, pipelines, calendars, funnels, workflows, invoices, custom objects, webhooks, and more. **Includes full workflow builder, funnel/page editor, form builder, pipeline builder, bulk operations, account export, and workflow cloning** — capabilities no other GHL tool offers.
|
|
4
4
|
|
|
5
5
|
**Distributed via npm as [`@elitedcs/ghl-mcp`](https://www.npmjs.com/package/@elitedcs/ghl-mcp).** Buyers install with one config block — no git, no Node.js setup, no terminal commands. Updates flow automatically (`npx @latest` re-resolves on every Claude restart).
|
|
6
6
|
|
|
@@ -98,7 +98,7 @@ Run setup_ghl_mcp to activate GHL Command:
|
|
|
98
98
|
ghl_location_id: YOUR_LOCATION_ID
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
Approve the tool call. Server validates your license, verifies your GHL credentials, writes them to a per-user config file. **Quit Claude one more time and reopen** — all
|
|
101
|
+
Approve the tool call. Server validates your license, verifies your GHL credentials, writes them to a per-user config file. **Quit Claude one more time and reopen** — all 169 tools are now unlocked.
|
|
102
102
|
|
|
103
103
|
### 4. Try it
|
|
104
104
|
|
|
@@ -138,7 +138,7 @@ https://app.gohighlevel.com/v2/location/YOUR_LOCATION_ID/dashboard
|
|
|
138
138
|
|
|
139
139
|
## Enable Workflow Builder (Optional)
|
|
140
140
|
|
|
141
|
-
The
|
|
141
|
+
The 9 workflow builder tools use GHL'''s internal API and require Firebase credentials. Without them, the other 168 tools work fine — you just won'''t have workflow editing.
|
|
142
142
|
|
|
143
143
|
Grab the three values from your GHL browser session, then re-run `setup_ghl_mcp` with them:
|
|
144
144
|
|
|
@@ -167,9 +167,9 @@ Re-run setup_ghl_mcp with workflow builder:
|
|
|
167
167
|
|
|
168
168
|
---
|
|
169
169
|
|
|
170
|
-
## Tools (
|
|
170
|
+
## Tools (178)
|
|
171
171
|
|
|
172
|
-
> **v3.
|
|
172
|
+
> **v3.7.0 fixes Don Harris's 2026-05-14 funnel-builder audit.** All six previously-broken funnel write tools (`update_funnel`, `create_funnel_page`, `delete_funnel_page`, `delete_funnel`, `update_page_content`, plus `create_funnel` which 401'd) now hit the correct GHL endpoints. New: `update_funnel_step` for per-step renames / slugs / domains. Origin + Referer headers added to all funnel writes — required by GHL's IAM, missing before.
|
|
173
173
|
|
|
174
174
|
### CRM & Contacts (15 tools)
|
|
175
175
|
|
|
@@ -334,7 +334,7 @@ Re-run setup_ghl_mcp with workflow builder:
|
|
|
334
334
|
| `get_subscriptions` | List active subscriptions |
|
|
335
335
|
| `get_transactions` | View transaction history |
|
|
336
336
|
|
|
337
|
-
### Workflow Builder (
|
|
337
|
+
### Workflow Builder (9 tools) — Internal API
|
|
338
338
|
|
|
339
339
|
> **This is the flagship feature.** GHL's public API has **zero** workflow write endpoints. These tools use GHL's internal backend API — the same API their own UI calls — reverse-engineered to give Claude full workflow CRUD. No other GHL integration, Zapier connector, or third-party tool can create or edit workflows programmatically. Requires additional Firebase auth setup (see "Enable Workflow Builder" in Quick Start above).
|
|
340
340
|
|
|
@@ -348,10 +348,11 @@ Re-run setup_ghl_mcp with workflow builder:
|
|
|
348
348
|
| `publish_workflow` | Publish a draft workflow to make it active |
|
|
349
349
|
| `get_trigger_registry` | Discover GHL's marketplace trigger apps (Zoom, WooCommerce, Shopify, etc.) and their available trigger templates |
|
|
350
350
|
| `validate_workflow` | Pre-flight validation: scans every action and trigger for references to pipelines, stages, custom fields, users, and other workflows; verifies each ID exists. Catches the silent-failure bug where invalid IDs make GHL skip subsequent actions |
|
|
351
|
+
| `build_goal_event` | Build a correctly-shaped goal-event node. Goals sit inline in the action chain — when the goal condition fires (e.g., review_request_clicked), the configured action runs (default: exit the workflow). Returns a `workflow_goal` node ready to wire into your actions array |
|
|
351
352
|
|
|
352
|
-
**What you can build:** Complete automation sequences — lead nurture drip campaigns, appointment reminder sequences, onboarding flows, re-engagement workflows, internal notification chains
|
|
353
|
+
**What you can build:** Complete automation sequences — lead nurture drip campaigns, appointment reminder sequences, onboarding flows, re-engagement workflows, internal notification chains, **conditional exit workflows with goal events**. Tell Claude what you want and it builds the workflow action by action.
|
|
353
354
|
|
|
354
|
-
**Supported action types:** `sms`, `email`, `add_contact_tag`, `remove_contact_tag`, `update_contact_field`, `wait`, `if_else`, `webhook`, `create_opportunity`, `custom_code`, `add_notes`, `internal_notification`, `
|
|
355
|
+
**Supported action types:** `sms`, `email`, `add_contact_tag`, `remove_contact_tag`, `update_contact_field`, `wait`, `if_else`, `webhook`, `create_opportunity`, `custom_code`, `add_notes`, `internal_notification`, `task_notification`, `remove_from_workflow`, **`workflow_goal`**, and more.
|
|
355
356
|
|
|
356
357
|
**Supported trigger types:** **All 57 native GHL trigger types are deeply typed** with their own Zod variant. 42 have full field-path documentation (`opportunity_*`, `form_submission`, `contact_changed`, `survey_submission`, `birthday_reminder`, `mailgun_email_event`, and 36 others). 4 have partial docs, 2 are fieldless by design, 9 have type-only discrimination pending field-path capture. A permissive fallback handles any future trigger types GHL ships, so reads never crash. Plus 434 marketplace trigger entries across 85 third-party apps — query them via `get_trigger_registry`.
|
|
357
358
|
|
|
@@ -419,7 +420,7 @@ Re-run setup_ghl_mcp with workflow builder:
|
|
|
419
420
|
|---|---|
|
|
420
421
|
| `get_mcp_version` | Check installed version against the latest published to npm. Confirms an upgrade landed after restarting Claude. Available even before GHL credentials are configured. |
|
|
421
422
|
| `health_check` | Run a full health check: npm registry + version status, GHL API key validity, default location reachability, Firebase auth status, token registry presence. Use when something feels broken. |
|
|
422
|
-
| `enable_workflow_builder` | Add Firebase credentials to an existing install to unlock the
|
|
423
|
+
| `enable_workflow_builder` | Add Firebase credentials to an existing install to unlock the 9 workflow builder tools. No need to re-enter license / API key / location ID. Run this any time after the basic setup, on the buyer's schedule. |
|
|
423
424
|
|
|
424
425
|
### Other Modules
|
|
425
426
|
|
|
@@ -541,7 +542,7 @@ ghl-command-mcp/
|
|
|
541
542
|
│ ├── emails.ts # 1 tool — email campaign listing
|
|
542
543
|
│ ├── trigger-links.ts # 1 tool — trigger link listing
|
|
543
544
|
│ ├── workflow-builder.ts # 6 tools — full workflow CRUD (internal API)
|
|
544
|
-
│ ├── funnel-builder.ts #
|
|
545
|
+
│ ├── funnel-builder.ts # 10 tools — funnel/page builder (internal API)
|
|
545
546
|
│ ├── form-builder.ts # 5 tools — form builder (internal API)
|
|
546
547
|
│ ├── pipeline-builder.ts # 5 tools — pipeline/stage builder (internal API)
|
|
547
548
|
│ ├── bulk-operations.ts # 5 tools — batch contact operations
|
package/dist/index.js
CHANGED
|
@@ -31,8 +31,8 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "@elitedcs/ghl-mcp",
|
|
34
|
-
version: "3.
|
|
35
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
34
|
+
version: "3.7.0",
|
|
35
|
+
description: "GoHighLevel MCP Server for Claude. 178 tools \u2014 full CRM, automation, marketing control, and the only programmatic GHL workflow builder.",
|
|
36
36
|
main: "dist/index.js",
|
|
37
37
|
bin: {
|
|
38
38
|
"ghl-mcp": "dist/index.js"
|
|
@@ -869,6 +869,14 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
869
869
|
getApiKey() {
|
|
870
870
|
return this.apiKey;
|
|
871
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* Return the current Firebase user ID. Required by the funnel-builder's
|
|
874
|
+
* delete_funnel endpoint which needs `userId` in the request body. Not
|
|
875
|
+
* exposed via MCP.
|
|
876
|
+
*/
|
|
877
|
+
getUserId() {
|
|
878
|
+
return this.userId;
|
|
879
|
+
}
|
|
872
880
|
/**
|
|
873
881
|
* Probe Firebase auth by refreshing the ID token. Used by the health_check
|
|
874
882
|
* diagnostic tool. Does not touch GHL backend — just exchanges the refresh
|
|
@@ -4137,7 +4145,7 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
4137
4145
|
);
|
|
4138
4146
|
server2.tool(
|
|
4139
4147
|
"update_workflow_actions",
|
|
4140
|
-
"Update a workflow's actions (steps), triggers, name, or status. IMPORTANT: Call get_workflow_full first to see the current state before updating. Handles version tracking automatically. Uses the internal builder API (requires Firebase auth). Action types: sms, email, add_contact_tag, remove_contact_tag, wait, webhook, internal_update_opportunity, custom_code, update_contact_field, add_notes, internal_notification, task_notification, remove_from_workflow, add_to_workflow, goto, transition. For if/else, call build_if_else_branch and include its returned nodes; if_else is a node discriminator, not a standalone action.
|
|
4148
|
+
"Update a workflow's actions (steps), triggers, name, or status. IMPORTANT: Call get_workflow_full first to see the current state before updating. Handles version tracking automatically. Uses the internal builder API (requires Firebase auth). Action types: sms, email, add_contact_tag, remove_contact_tag, wait, webhook, internal_update_opportunity, custom_code, update_contact_field, add_notes, internal_notification, task_notification, remove_from_workflow, add_to_workflow, goto, transition, workflow_goal. For if/else, call build_if_else_branch and include its returned nodes; if_else is a node discriminator, not a standalone action. For goal events (exit-on-condition nodes), call build_goal_event to get a correctly-shaped workflow_goal node \u2014 wire it in by setting the prior action's `next` to the goal node's id. Trigger types: all 57 native GHL trigger types have typed validation; any unknown trigger type passes through via a permissive fallback so reads never crash.",
|
|
4141
4149
|
{
|
|
4142
4150
|
workflowId: import_zod31.z.string().describe("The workflow ID to update."),
|
|
4143
4151
|
name: import_zod31.z.string().optional().describe("New workflow name."),
|
|
@@ -4298,6 +4306,53 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
4298
4306
|
}
|
|
4299
4307
|
}
|
|
4300
4308
|
);
|
|
4309
|
+
server2.tool(
|
|
4310
|
+
"build_goal_event",
|
|
4311
|
+
"Build a correctly-shaped GHL workflow goal-event node. Goal events sit inline in the action chain \u2014 when the goal condition fires during workflow execution, the configured action runs (default: exit the workflow). The previous action's `next` should point to the goal node's id; the goal node itself does not have a `next`. Verified goal_condition value (Jerry's UI sample): 'review_request_clicked' with extras { reviewTypes: ['sms', 'email'], reviewLinkId: '' }. Other goal_condition values that GHL ships are passed through verbatim \u2014 the catalogue of all goal conditions has not been fully captured yet, but the schema accepts any string value.",
|
|
4312
|
+
{
|
|
4313
|
+
goal_condition: import_zod31.z.string().describe("The goal-condition identifier \u2014 what the goal is watching for. Verified value: 'review_request_clicked'. Other goal_condition strings that GHL's UI exposes (e.g., tag-related, appointment-related) pass through unchanged."),
|
|
4314
|
+
extras: import_zod31.z.record(import_zod31.z.unknown()).optional().describe("Goal-condition-specific config. Example for review_request_clicked: { reviewTypes: ['sms', 'email'], reviewLinkId: '' }. Pass an empty object {} or omit if the goal_condition has no extras."),
|
|
4315
|
+
action: import_zod31.z.enum(["exit", "continue", "goto"]).default("exit").describe("What happens when the goal fires. 'exit' terminates the workflow path (verified). 'continue' and 'goto' are GHL UI options; 'goto' likely needs an extra targetNodeId field, not yet captured."),
|
|
4316
|
+
target_node_id: import_zod31.z.string().optional().describe("Required when action === 'goto'. The action node id to jump to."),
|
|
4317
|
+
name: import_zod31.z.string().default("Goal").describe("Display name in the GHL UI."),
|
|
4318
|
+
op: import_zod31.z.enum(["or", "and"]).default("or").describe("Top-level boolean operator across segments. Default 'or'."),
|
|
4319
|
+
inner_op: import_zod31.z.enum(["or", "and"]).default("or").describe("Boolean operator across conditions within a single segment. Default 'or'.")
|
|
4320
|
+
},
|
|
4321
|
+
async ({ goal_condition, extras, action, target_node_id, name, op, inner_op }) => {
|
|
4322
|
+
try {
|
|
4323
|
+
if (action === "goto" && !target_node_id) {
|
|
4324
|
+
throw new Error("action='goto' requires target_node_id to identify which workflow action to jump to.");
|
|
4325
|
+
}
|
|
4326
|
+
const nodeId = crypto.randomUUID();
|
|
4327
|
+
const conditionId = crypto.randomUUID();
|
|
4328
|
+
const attributes = {
|
|
4329
|
+
op,
|
|
4330
|
+
segments: [{
|
|
4331
|
+
op: inner_op,
|
|
4332
|
+
conditions: [{
|
|
4333
|
+
goal_condition,
|
|
4334
|
+
extras: extras ?? {},
|
|
4335
|
+
id: conditionId
|
|
4336
|
+
}]
|
|
4337
|
+
}],
|
|
4338
|
+
type: "workflow_goal",
|
|
4339
|
+
action
|
|
4340
|
+
};
|
|
4341
|
+
if (action === "goto" && target_node_id) {
|
|
4342
|
+
attributes.targetNodeId = target_node_id;
|
|
4343
|
+
}
|
|
4344
|
+
const goalNode = {
|
|
4345
|
+
id: nodeId,
|
|
4346
|
+
name,
|
|
4347
|
+
type: "workflow_goal",
|
|
4348
|
+
attributes
|
|
4349
|
+
};
|
|
4350
|
+
return jsonResponse(goalNode);
|
|
4351
|
+
} catch (error) {
|
|
4352
|
+
return errorResponse(error);
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
);
|
|
4301
4356
|
server2.tool(
|
|
4302
4357
|
"delete_workflow_full",
|
|
4303
4358
|
"Permanently delete a workflow. IRREVERSIBLE.",
|
|
@@ -4350,6 +4405,8 @@ function registerFunnelBuilderTools(server2, builderClient) {
|
|
|
4350
4405
|
}
|
|
4351
4406
|
async function funnelRequest(method, path6, body) {
|
|
4352
4407
|
const headers = await client.buildHeaders();
|
|
4408
|
+
headers.Origin = "https://app.gohighlevel.com";
|
|
4409
|
+
headers.Referer = "https://app.gohighlevel.com/";
|
|
4353
4410
|
const url = `https://backend.leadconnectorhq.com/funnels${path6}`;
|
|
4354
4411
|
const options = { method, headers };
|
|
4355
4412
|
if (body && (method === "POST" || method === "PUT")) {
|
|
@@ -4455,21 +4512,92 @@ ${text2}`);
|
|
|
4455
4512
|
);
|
|
4456
4513
|
server2.tool(
|
|
4457
4514
|
"update_funnel",
|
|
4458
|
-
"Update a funnel's name,
|
|
4515
|
+
"Update a funnel's settings: name, path/slug, tracking codes, GDPR + payment + chat widget flags, etc. This is the 'save settings panel' endpoint \u2014 it sends the entire settings object, so the tool fetches current settings first, merges your overrides on top, then POSTs back. To rename a single step (page) inside a funnel, use update_funnel_step instead.",
|
|
4459
4516
|
{
|
|
4460
4517
|
funnelId: import_zod32.z.string().describe("The funnel ID to update."),
|
|
4461
|
-
name: import_zod32.z.string().optional().describe("New funnel name."),
|
|
4462
|
-
|
|
4518
|
+
name: import_zod32.z.string().optional().describe("New funnel display name."),
|
|
4519
|
+
path: import_zod32.z.string().optional().describe("URL slug for the funnel (e.g., '/my-funnel'). Mapped to funnelPath."),
|
|
4520
|
+
bodyTrackingCode: import_zod32.z.string().optional().describe("Custom tracking script injected before </body>."),
|
|
4521
|
+
headTrackingCode: import_zod32.z.string().optional().describe("Custom tracking script injected before </head>."),
|
|
4522
|
+
chatWidgetId: import_zod32.z.string().optional().describe("Chat widget to embed on every page in this funnel."),
|
|
4523
|
+
domainId: import_zod32.z.string().optional().describe("Custom domain ID for the funnel."),
|
|
4524
|
+
faviconUrl: import_zod32.z.string().optional().describe("Favicon URL for funnel pages."),
|
|
4525
|
+
paymentMode: import_zod32.z.boolean().optional().describe("Whether to require a payment mode for the funnel."),
|
|
4526
|
+
requireCreditCard: import_zod32.z.boolean().optional().describe("Whether to require a credit card on opt-in steps."),
|
|
4527
|
+
isGdprCompliant: import_zod32.z.boolean().optional().describe("Toggle GDPR compliance mode."),
|
|
4528
|
+
isOptimisePageLoad: import_zod32.z.boolean().optional().describe("Enable GHL's page-load optimization."),
|
|
4529
|
+
imageOptimization: import_zod32.z.boolean().optional().describe("Enable automatic image optimization."),
|
|
4530
|
+
allowPaymentModeOption: import_zod32.z.boolean().optional().describe("Allow buyers to choose payment mode on checkout."),
|
|
4531
|
+
storeCurrencyFormatting: import_zod32.z.boolean().optional().describe("Use store-wide currency formatting.")
|
|
4463
4532
|
},
|
|
4464
|
-
async (
|
|
4533
|
+
async (args) => {
|
|
4465
4534
|
try {
|
|
4466
|
-
const
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4535
|
+
const current = await funnelRequest("GET", `/funnel/list?locationId=${client.locationId}&limit=200`);
|
|
4536
|
+
const funnels = Array.isArray(current.funnels) ? current.funnels : [];
|
|
4537
|
+
const found = funnels.find((f) => f._id === args.funnelId || f.id === args.funnelId);
|
|
4538
|
+
if (!found) {
|
|
4539
|
+
throw new Error(`Funnel ${args.funnelId} not found in this location.`);
|
|
4540
|
+
}
|
|
4541
|
+
const overrides = {};
|
|
4542
|
+
if (args.name !== void 0) overrides.funnelName = args.name;
|
|
4543
|
+
if (args.path !== void 0) overrides.funnelPath = args.path;
|
|
4544
|
+
if (args.bodyTrackingCode !== void 0) overrides.bodyTrackingCode = args.bodyTrackingCode;
|
|
4545
|
+
if (args.headTrackingCode !== void 0) overrides.headTrackingCode = args.headTrackingCode;
|
|
4546
|
+
if (args.chatWidgetId !== void 0) overrides.chatWidgetId = args.chatWidgetId;
|
|
4547
|
+
if (args.domainId !== void 0) overrides.domainId = args.domainId;
|
|
4548
|
+
if (args.faviconUrl !== void 0) overrides.faviconUrl = args.faviconUrl;
|
|
4549
|
+
if (args.paymentMode !== void 0) overrides.paymentMode = args.paymentMode;
|
|
4550
|
+
if (args.requireCreditCard !== void 0) overrides.requireCreditCard = args.requireCreditCard;
|
|
4551
|
+
if (args.isGdprCompliant !== void 0) overrides.isGdprCompliant = args.isGdprCompliant;
|
|
4552
|
+
if (args.isOptimisePageLoad !== void 0) overrides.isOptimisePageLoad = args.isOptimisePageLoad;
|
|
4553
|
+
if (args.imageOptimization !== void 0) overrides.imageOptimization = args.imageOptimization;
|
|
4554
|
+
if (args.allowPaymentModeOption !== void 0) overrides.allowPaymentModeOption = args.allowPaymentModeOption;
|
|
4555
|
+
if (args.storeCurrencyFormatting !== void 0) overrides.storeCurrencyFormatting = args.storeCurrencyFormatting;
|
|
4556
|
+
const body = {
|
|
4557
|
+
locationId: client.locationId,
|
|
4558
|
+
funnelId: args.funnelId,
|
|
4559
|
+
funnelName: found.name ?? "",
|
|
4560
|
+
funnelPath: found.url ?? "/",
|
|
4561
|
+
allowPaymentModeOption: found.allowPaymentModeOption ?? true,
|
|
4562
|
+
bodyTrackingCode: found.bodyTrackingCode ?? "",
|
|
4563
|
+
chatWidgetId: found.chatWidgetId ?? "",
|
|
4564
|
+
domainId: found.domainId ?? "",
|
|
4565
|
+
faviconUrl: found.faviconUrl ?? "",
|
|
4566
|
+
headTrackingCode: found.headTrackingCode ?? "",
|
|
4567
|
+
imageOptimization: found.imageOptimization ?? true,
|
|
4568
|
+
isGdprCompliant: found.isGdprCompliant ?? false,
|
|
4569
|
+
isOptimisePageLoad: found.isOptimisePageLoad ?? true,
|
|
4570
|
+
paymentMode: found.paymentMode ?? true,
|
|
4571
|
+
requireCreditCard: found.requireCreditCard ?? true,
|
|
4572
|
+
stopAllSplitTestsAndReset: null,
|
|
4573
|
+
storeCurrencyFormatting: found.storeCurrencyFormatting ?? false,
|
|
4574
|
+
...overrides
|
|
4472
4575
|
};
|
|
4576
|
+
const result = await funnelRequest("POST", `/funnel/update-settings`, body);
|
|
4577
|
+
return jsonResponse(result);
|
|
4578
|
+
} catch (error) {
|
|
4579
|
+
return errorResponse(error);
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
);
|
|
4583
|
+
server2.tool(
|
|
4584
|
+
"update_funnel_step",
|
|
4585
|
+
"Update a single step (page entry) inside a funnel \u2014 display name, URL slug, or attached domain. A step is GHL's container for one or more pages (multiple pages exist when split-testing). Use update_funnel for funnel-level settings instead.",
|
|
4586
|
+
{
|
|
4587
|
+
funnelId: import_zod32.z.string().describe("The funnel ID containing the step."),
|
|
4588
|
+
stepId: import_zod32.z.string().describe("The step ID to update (from get_funnel_pages / list_funnels_full)."),
|
|
4589
|
+
name: import_zod32.z.string().optional().describe("New step display name."),
|
|
4590
|
+
url: import_zod32.z.string().optional().describe("URL slug for the step (e.g., '/thank-you')."),
|
|
4591
|
+
domainName: import_zod32.z.string().optional().describe("Custom domain to attach to this step.")
|
|
4592
|
+
},
|
|
4593
|
+
async ({ funnelId, stepId, name, url, domainName }) => {
|
|
4594
|
+
try {
|
|
4595
|
+
const body = { stepId };
|
|
4596
|
+
if (name !== void 0) body.name = name;
|
|
4597
|
+
if (url !== void 0) body.url = url;
|
|
4598
|
+
if (domainName !== void 0) body.domainName = domainName;
|
|
4599
|
+
const result = await funnelRequest("PUT", `/funnel/step/${funnelId}`, body);
|
|
4600
|
+
return jsonResponse(result);
|
|
4473
4601
|
} catch (error) {
|
|
4474
4602
|
return errorResponse(error);
|
|
4475
4603
|
}
|
|
@@ -4477,17 +4605,19 @@ ${text2}`);
|
|
|
4477
4605
|
);
|
|
4478
4606
|
server2.tool(
|
|
4479
4607
|
"delete_funnel",
|
|
4480
|
-
"Permanently delete a funnel and all its pages. IRREVERSIBLE.",
|
|
4608
|
+
"Permanently delete a funnel and all its steps/pages. IRREVERSIBLE.",
|
|
4481
4609
|
{
|
|
4482
4610
|
funnelId: import_zod32.z.string().describe("The funnel ID to delete."),
|
|
4483
4611
|
confirm: import_zod32.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action.")
|
|
4484
4612
|
},
|
|
4485
4613
|
async ({ funnelId }) => {
|
|
4486
4614
|
try {
|
|
4487
|
-
const result = await funnelRequest("
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4615
|
+
const result = await funnelRequest("POST", `/funnel/delete`, {
|
|
4616
|
+
funnelId,
|
|
4617
|
+
locationId: client.locationId,
|
|
4618
|
+
userId: client.getUserId()
|
|
4619
|
+
});
|
|
4620
|
+
return jsonResponse(result);
|
|
4491
4621
|
} catch (error) {
|
|
4492
4622
|
return errorResponse(error);
|
|
4493
4623
|
}
|
|
@@ -4495,23 +4625,30 @@ ${text2}`);
|
|
|
4495
4625
|
);
|
|
4496
4626
|
server2.tool(
|
|
4497
4627
|
"create_funnel_page",
|
|
4498
|
-
"Create a new page in a funnel.
|
|
4628
|
+
"Create a new step (page entry) in a funnel. The client generates a step UUID locally and sends it in the request; GHL auto-creates a blank page inside the step on the server side. The response includes the new step's id and the page's Firestore reference, which you can then update via update_page_content.",
|
|
4499
4629
|
{
|
|
4500
|
-
funnelId: import_zod32.z.string().describe("The funnel ID to add the
|
|
4501
|
-
name: import_zod32.z.string().describe("
|
|
4502
|
-
url: import_zod32.z.string().optional().describe("URL slug for the
|
|
4630
|
+
funnelId: import_zod32.z.string().describe("The funnel ID to add the step to."),
|
|
4631
|
+
name: import_zod32.z.string().describe("Step display name."),
|
|
4632
|
+
url: import_zod32.z.string().optional().describe("URL slug for the step. Defaults to '' (blank) like the GHL UI."),
|
|
4633
|
+
type: import_zod32.z.string().optional().describe("Step type. Defaults to 'optin_funnel_page'.")
|
|
4503
4634
|
},
|
|
4504
|
-
async ({ funnelId, name, url }) => {
|
|
4635
|
+
async ({ funnelId, name, url, type }) => {
|
|
4505
4636
|
try {
|
|
4506
|
-
const
|
|
4637
|
+
const stepId = crypto.randomUUID();
|
|
4638
|
+
const result = await funnelRequest("POST", `/funnel/create-step`, {
|
|
4507
4639
|
funnelId,
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4640
|
+
step: {
|
|
4641
|
+
id: stepId,
|
|
4642
|
+
name,
|
|
4643
|
+
url: url ?? "",
|
|
4644
|
+
pages: [],
|
|
4645
|
+
control_traffic: 100,
|
|
4646
|
+
split: false,
|
|
4647
|
+
type: type ?? "optin_funnel_page"
|
|
4648
|
+
}
|
|
4511
4649
|
});
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
};
|
|
4650
|
+
const merged = typeof result === "object" && result !== null ? { stepId, ...result } : { stepId, result };
|
|
4651
|
+
return jsonResponse(merged);
|
|
4515
4652
|
} catch (error) {
|
|
4516
4653
|
return errorResponse(error);
|
|
4517
4654
|
}
|
|
@@ -4519,20 +4656,22 @@ ${text2}`);
|
|
|
4519
4656
|
);
|
|
4520
4657
|
server2.tool(
|
|
4521
4658
|
"update_page_content",
|
|
4522
|
-
"Update a page's builder content \u2014 sections, elements,
|
|
4659
|
+
"Update a page's builder content \u2014 sections, elements, settings, pageStyles, trackingCode, popups, popupsList, fontsForPreview. Use get_page_content first to see the current structure, modify it, then pass the full 8-field envelope here. GHL's UI fires three POSTs to this endpoint per save (one collaborative-edit broadcast + two commits); for MCP use, one POST with write=true is enough.",
|
|
4523
4660
|
{
|
|
4524
4661
|
pageId: import_zod32.z.string().describe("The page ID to update."),
|
|
4525
|
-
content: import_zod32.z.record(import_zod32.z.unknown()).describe("The full page content JSON
|
|
4662
|
+
content: import_zod32.z.record(import_zod32.z.unknown()).describe("The full page content JSON. Expected fields: sections, settings, general, pageStyles, trackingCode, popups, popupsList, fontsForPreview."),
|
|
4663
|
+
isPublished: import_zod32.z.boolean().optional().describe("Whether the page should be marked published after the save. Defaults to false (draft).")
|
|
4526
4664
|
},
|
|
4527
|
-
async ({ pageId, content }) => {
|
|
4665
|
+
async ({ pageId, content, isPublished }) => {
|
|
4528
4666
|
try {
|
|
4529
|
-
const result = await funnelRequest("
|
|
4530
|
-
|
|
4531
|
-
|
|
4667
|
+
const result = await funnelRequest("POST", `/builder/prebuilt-section/sync/changes`, {
|
|
4668
|
+
locationId: client.locationId,
|
|
4669
|
+
pageId,
|
|
4670
|
+
pageData: content,
|
|
4671
|
+
write: true,
|
|
4672
|
+
isPublished: isPublished ?? false
|
|
4532
4673
|
});
|
|
4533
|
-
return
|
|
4534
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
4535
|
-
};
|
|
4674
|
+
return jsonResponse(result);
|
|
4536
4675
|
} catch (error) {
|
|
4537
4676
|
return errorResponse(error);
|
|
4538
4677
|
}
|
|
@@ -4540,17 +4679,16 @@ ${text2}`);
|
|
|
4540
4679
|
);
|
|
4541
4680
|
server2.tool(
|
|
4542
4681
|
"delete_funnel_page",
|
|
4543
|
-
"Permanently delete a
|
|
4682
|
+
"Permanently delete a step (and all its pages) from a funnel. IRREVERSIBLE. SIGNATURE CHANGED in v3.7.0: was pageId, now requires funnelId + stepId to match GHL's actual delete-step endpoint.",
|
|
4544
4683
|
{
|
|
4545
|
-
|
|
4684
|
+
funnelId: import_zod32.z.string().describe("The funnel ID containing the step."),
|
|
4685
|
+
stepId: import_zod32.z.string().describe("The step ID to delete (from get_funnel_pages / list_funnels_full)."),
|
|
4546
4686
|
confirm: import_zod32.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action.")
|
|
4547
4687
|
},
|
|
4548
|
-
async ({
|
|
4688
|
+
async ({ funnelId, stepId }) => {
|
|
4549
4689
|
try {
|
|
4550
|
-
const result = await funnelRequest("
|
|
4551
|
-
return
|
|
4552
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
4553
|
-
};
|
|
4690
|
+
const result = await funnelRequest("POST", `/funnel/delete-step`, { funnelId, stepId });
|
|
4691
|
+
return jsonResponse(result);
|
|
4554
4692
|
} catch (error) {
|
|
4555
4693
|
return errorResponse(error);
|
|
4556
4694
|
}
|
|
@@ -6586,7 +6724,7 @@ async function validateFirebase(firebaseKey, refreshToken) {
|
|
|
6586
6724
|
function registerSetupTool(server2) {
|
|
6587
6725
|
server2.tool(
|
|
6588
6726
|
"setup_ghl_mcp",
|
|
6589
|
-
"First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all
|
|
6727
|
+
"First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 178 tools (168 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
|
|
6590
6728
|
{
|
|
6591
6729
|
email: import_zod42.z.string().email().describe("Email used at purchase."),
|
|
6592
6730
|
license_key: import_zod42.z.string().min(20).describe("License key from your purchase email."),
|
|
@@ -6641,7 +6779,7 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
|
|
|
6641
6779
|
ghl_firebase_api_key: workflowBuilderEnabled ? args.ghl_firebase_api_key?.trim() : void 0,
|
|
6642
6780
|
ghl_firebase_refresh_token: workflowBuilderEnabled ? args.ghl_firebase_refresh_token?.trim() : void 0
|
|
6643
6781
|
});
|
|
6644
|
-
const toolCount = workflowBuilderEnabled ? "
|
|
6782
|
+
const toolCount = workflowBuilderEnabled ? "178" : "168";
|
|
6645
6783
|
const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
|
|
6646
6784
|
const wfTip = workflowBuilderEnabled ? "" : "\nTo enable Workflow Builder later (8 extra tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
|
|
6647
6785
|
return {
|
|
@@ -6669,7 +6807,7 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
|
|
|
6669
6807
|
function registerEnableWorkflowBuilderTool(server2) {
|
|
6670
6808
|
server2.tool(
|
|
6671
6809
|
"enable_workflow_builder",
|
|
6672
|
-
"Add Firebase credentials to an existing GHL Command install to unlock the
|
|
6810
|
+
"Add Firebase credentials to an existing GHL Command install to unlock the 9 workflow builder tools (create/edit/clone/delete/publish workflows, validate_workflow, get_trigger_registry). Requires you've already run setup_ghl_mcp. Capture the three Firebase values from your GHL browser session \u2014 see elitedcs.com/ghl-mcp-firebase for step-by-step DevTools instructions. Tool count goes from 168 to 178 after the next Claude restart.",
|
|
6673
6811
|
{
|
|
6674
6812
|
ghl_user_id: import_zod42.z.string().min(10).describe("Firebase User ID (uid). DevTools \u2192 Application \u2192 IndexedDB \u2192 firebaseLocalStorageDb \u2192 firebaseLocalStorage \u2192 the value.uid field of the firebase:authUser row."),
|
|
6675
6813
|
ghl_firebase_api_key: import_zod42.z.string().min(10).describe("Firebase API Key starting with 'AIza'. The string between 'firebase:authUser:' and ':[DEFAULT]' in the row's Key column."),
|
|
@@ -6716,7 +6854,7 @@ DevTools steps: https://elitedcs.com/ghl-mcp-firebase`
|
|
|
6716
6854
|
"",
|
|
6717
6855
|
"Firebase credentials verified and saved.",
|
|
6718
6856
|
"",
|
|
6719
|
-
"**Restart Claude (quit fully and reopen) to load the
|
|
6857
|
+
"**Restart Claude (quit fully and reopen) to load the workflow builder + funnel builder + pipeline builder + form builder + workflow cloner tools (178 total).**",
|
|
6720
6858
|
"",
|
|
6721
6859
|
'After restart, try: "List my workflows in full detail" or "Validate workflow <id>".',
|
|
6722
6860
|
"",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "GoHighLevel MCP Server for Claude.
|
|
3
|
+
"version": "3.7.0",
|
|
4
|
+
"description": "GoHighLevel MCP Server for Claude. 178 tools — full CRM, automation, marketing control, and the only programmatic GHL workflow builder.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ghl-mcp": "dist/index.js"
|
|
@@ -247,6 +247,33 @@
|
|
|
247
247
|
"branch_notes": "Branch nodes need BOTH parentKey AND parent (same value). Sibling arrays cross-reference the other branch. Condition node next points to [YES, NO]. Branch node next is a string to the first child when non-empty. CAUTION: Complex branching (>2 if/else in one workflow) can freeze GHL's UI renderer. Use separate exit workflows for nurture sequences instead."
|
|
248
248
|
},
|
|
249
249
|
|
|
250
|
+
"workflow_goal": {
|
|
251
|
+
"example": {
|
|
252
|
+
"id": "<UUID>",
|
|
253
|
+
"name": "Goal",
|
|
254
|
+
"type": "workflow_goal",
|
|
255
|
+
"attributes": {
|
|
256
|
+
"op": "or",
|
|
257
|
+
"segments": [
|
|
258
|
+
{
|
|
259
|
+
"op": "or",
|
|
260
|
+
"conditions": [
|
|
261
|
+
{
|
|
262
|
+
"goal_condition": "review_request_clicked",
|
|
263
|
+
"extras": { "reviewTypes": ["sms", "email"], "reviewLinkId": "" },
|
|
264
|
+
"id": "<UUID>"
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
],
|
|
269
|
+
"type": "workflow_goal",
|
|
270
|
+
"action": "exit"
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
"notes": "Goal-event nodes sit inline in the action chain — when the goal condition fires during workflow execution, the configured action runs (default: 'exit' terminates the workflow path). The previous action's `next` should point to the goal node's id; the goal node itself does NOT have a `next` field. Use build_goal_event to construct the node — it handles UUID generation and the nested op/segments/conditions shape. Verified goal_condition value: 'review_request_clicked' (with extras { reviewTypes: ['sms', 'email'], reviewLinkId: '' }). Other goal_condition strings from GHL's UI pass through verbatim; the full catalogue of goal conditions has not been captured yet.",
|
|
274
|
+
"build_helper": "build_goal_event(goal_condition, extras?, action='exit', target_node_id?, name='Goal', op='or', inner_op='or')"
|
|
275
|
+
},
|
|
276
|
+
|
|
250
277
|
"_trigger_patterns": {
|
|
251
278
|
"real_shape": {
|
|
252
279
|
"id": "<UUID>",
|