@elitedcs/ghl-mcp 3.7.0 → 3.8.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 CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.8.0 — Bundle re-extraction wins (triggers + goal events) + correctness fixes
4
+
5
+ **178 tools across 38 modules. Bundle: 291.3 KB.**
6
+
7
+ Three improvements that all flow from one bundle dive into `client-app-automation-workflows.leadconnectorhq.com`:
8
+
9
+ ### Goal-event catalogue — all 10 conditions documented (was 1)
10
+ v3.6.0 shipped `build_goal_event` with one verified `goal_condition` (`review_request_clicked`) and a permissive string for everything else. The bundle's `GoalCondition` enum reveals the full set:
11
+
12
+ - `email_event` — extras: `{ stepIds: [] }`
13
+ - `link_click` — extras: `{ linkIds: [] }`
14
+ - `add_contact_tag` — extras: `{ tags: [] }` (inferred)
15
+ - `remove_contact_tag` — extras: `{ tags: [] }` (inferred)
16
+ - `appointment_status` — extras: `{ calendarId }`
17
+ - `payment_received` — extras: `{ globalProductIds: [] }`
18
+ - `form_submission` — extras: `{ formIds: [] }`
19
+ - `document_status` — extras: `{ templateId }`
20
+ - `invoice_paid` — extras: `{ invoiceStepId }`
21
+ - `review_request_clicked` — extras: `{ reviewTypes, reviewLinkId }`
22
+
23
+ `build_goal_event`'s `goal_condition` is now a `z.enum` of these 10 values (was permissive `z.string()`). The per-condition extras shapes are documented in the tool description.
24
+
25
+ ### Goal-event action enum fixed
26
+ v3.6.0's tool advertised `action: "exit" | "continue" | "goto"`. The actual `GoalAction` enum in the bundle is `continue | wait | exit` — **no `goto`**. Fixed. Also removed the `target_node_id` parameter that paired with the bogus goto. `WorkflowGoalAttributes` type updated to match.
27
+
28
+ ### 7 of 9 previously-uncaptured triggers now documented
29
+ Trigger field paths extracted from per-trigger validator functions in the bundle:
30
+
31
+ - `affiliate_created` → `affiliate.id, contact.tags`
32
+ - `scheduler_trigger` → `scheduler.interval, scheduler.cron, scheduler.frequency`
33
+ - `user_log_in` → `product.id, category.id, lesson.id, offer.id, contact.tags` (shares membership-course validator)
34
+ - `order_submission` → `order.funnel_id, order.line_item_global_product_ids, order.line_item_funnel_product_ids, payment.calendar.id, payment.global_product_ids, payment.form.id`
35
+ - `facebook_lead_gen` → `facebook.formId, facebook.pageId, contact.tags`
36
+
37
+ Plus 2 more handled explicitly:
38
+ - `conv_ai_trigger`, `conv_ai_autonomous_trigger` — confirmed fieldless (no specific validator in the bundle)
39
+ - `custom_object_created`, `custom_object_changed` — fields are user-defined per object using the `customObject.<field>` prefix; schema now documents this dynamic shape
40
+
41
+ Coverage: **57/57 native triggers, 0 type-only-pending** (was 9 type-only-pending).
42
+
43
+ ### Bug fix: `remove_from_workflow` read/write asymmetry
44
+ Don Harris flagged at the end of his 2026-05-14 funnel audit that some GHL workflows return only the `workflow_id` array on read, while writes require both that AND the `workflowId` string. Couldn't reproduce in MCP Testing — both reads I checked returned both fields — but added defensive symmetric normalization to both paths in `workflow-builder-client.ts`. If only one form is present in either direction, the other is synthesized. No-op when both are already present.
45
+
46
+ ### Bug fix: accurate "without Firebase" tool count
47
+ Empirical audit: basic-creds mode (no Firebase) actually exposes **148 tools**, not 168 as the v3.5.1 messaging claimed. Firebase adds **30 tools** across 6 modules (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, validate_workflow). `setup_ghl_mcp`, `enable_workflow_builder`, README, and CLAUDE.md all corrected.
48
+
49
+ ### Bundle re-extraction methodology (for future audits)
50
+ 1. Fetch `https://client-app-automation-workflows.leadconnectorhq.com/` → find current `index-*.js` bundle URL
51
+ 2. Pull the bundle (~9.5 MB minified)
52
+ 3. Search for `var GoalCondition=(...)` and `var GoalAction=(...)` for enum extraction
53
+ 4. Search for `<triggerName>Validator=n=>` patterns for per-trigger field paths
54
+ 5. Watch for i18n translation pollution — filter out windows containing non-ASCII Danish/German/Swedish characters
55
+
56
+ ### Files changed
57
+ - `src/trigger-schemas.ts` — 7 uncaptured triggers now documented; 2 confirmed fieldless; 2 use dynamic `customObject.*` schema
58
+ - `src/tools/workflow-builder.ts` — `build_goal_event` enum tightened; goto removed; description lists all 10 goal_condition values + extras shapes
59
+ - `src/workflow-action-types.ts` — `WorkflowGoalAttributes.action` corrected to `continue | wait | exit`
60
+ - `src/workflow-builder-client.ts` — `normalizeRemoveFromWorkflowAction()` helper applied symmetrically on read + write
61
+ - `src/setup-tool.ts` — 168 → 148; "9 workflow builder tools" → "30 tools across 6 modules"
62
+ - `README.md`, `CLAUDE.md` — synced
63
+
3
64
  ## 3.7.0 — Funnel-builder fixes (Don Harris audit)
4
65
 
5
66
  **178 tools across 38 modules. Bundle: 288.4 KB.**
package/README.md CHANGED
@@ -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 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.
141
+ The 30 builder + cloner + validator tools (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, validate_workflow) use GHL'''s internal API and require Firebase credentials. Without them, the other 148 tools work fine — you just won'''t have workflow/funnel/form/pipeline editing.
142
142
 
143
143
  Grab the three values from your GHL browser session, then re-run `setup_ghl_mcp` with them:
144
144
 
@@ -420,7 +420,7 @@ Re-run setup_ghl_mcp with workflow builder:
420
420
  |---|---|
421
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. |
422
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. |
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
+ | `enable_workflow_builder` | Add Firebase credentials to an existing install to unlock 30 additional tools across 6 modules (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, validate_workflow). No need to re-enter license / API key / location ID. Run this any time after the basic setup, on the buyer's schedule. |
424
424
 
425
425
  ### Other Modules
426
426
 
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@elitedcs/ghl-mcp",
34
- version: "3.7.0",
34
+ version: "3.8.0",
35
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: {
@@ -651,15 +651,60 @@ var FacebookCommentOnPostTriggerSchema = typedTrigger("facebook_comment_on_post"
651
651
  var IgCommentOnPostTriggerSchema = typedTrigger("ig_comment_on_post", [], "partial");
652
652
  var InboundWebhookTriggerSchema = typedTrigger("inbound_webhook", [], "fieldless");
653
653
  var PaymentReceivedTriggerSchema = typedTrigger("payment_received", [], "fieldless");
654
- var AffiliateCreatedTriggerSchema = typedTrigger("affiliate_created", [], "uncaptured");
655
- var SchedulerTriggerTriggerSchema = typedTrigger("scheduler_trigger", [], "uncaptured");
656
- var UserLogInTriggerSchema = typedTrigger("user_log_in", [], "uncaptured");
657
- var OrderSubmissionTriggerSchema = typedTrigger("order_submission", [], "uncaptured");
658
- var ConvAiTriggerTriggerSchema = typedTrigger("conv_ai_trigger", [], "uncaptured");
659
- var ConvAiAutonomousTriggerTriggerSchema = typedTrigger("conv_ai_autonomous_trigger", [], "uncaptured");
660
- var CustomObjectCreatedTriggerSchema = typedTrigger("custom_object_created", [], "uncaptured");
661
- var CustomObjectChangedTriggerSchema = typedTrigger("custom_object_changed", [], "uncaptured");
662
- var FacebookLeadGenTriggerSchema = typedTrigger("facebook_lead_gen", [], "uncaptured");
654
+ var AffiliateCreatedTriggerSchema = typedTrigger("affiliate_created", [
655
+ "affiliate.id",
656
+ "contact.tags"
657
+ ]);
658
+ var SchedulerTriggerTriggerSchema = typedTrigger("scheduler_trigger", [
659
+ "scheduler.interval",
660
+ "scheduler.cron",
661
+ "scheduler.frequency"
662
+ ]);
663
+ var UserLogInTriggerSchema = typedTrigger("user_log_in", [
664
+ // Shares membershipCourseValidator with course/lesson/category triggers.
665
+ "product.id",
666
+ "category.id",
667
+ "lesson.id",
668
+ "offer.id",
669
+ "contact.tags"
670
+ ]);
671
+ var OrderSubmissionTriggerSchema = typedTrigger("order_submission", [
672
+ "order.funnel_id",
673
+ "order.line_item_global_product_ids",
674
+ "order.line_item_funnel_product_ids",
675
+ "payment.calendar.id",
676
+ "payment.global_product_ids",
677
+ "payment.form.id"
678
+ ]);
679
+ var FacebookLeadGenTriggerSchema = typedTrigger("facebook_lead_gen", [
680
+ "facebook.formId",
681
+ "facebook.pageId",
682
+ "contact.tags"
683
+ ]);
684
+ var CustomObjectCreatedTriggerSchema = TriggerCommonSchema.extend({
685
+ type: import_zod2.z.literal("custom_object_created"),
686
+ conditions: import_zod2.z.array(import_zod2.z.object({
687
+ operator: import_zod2.z.string(),
688
+ field: import_zod2.z.string().describe("Custom-object field paths use the `customObject.<your_field_name>` prefix. Field names are user-defined per custom object \u2014 query the location's custom objects to discover valid field paths."),
689
+ value: import_zod2.z.unknown().optional(),
690
+ title: import_zod2.z.string().optional(),
691
+ type: import_zod2.z.string().optional(),
692
+ id: import_zod2.z.string().optional()
693
+ }).passthrough()).optional()
694
+ });
695
+ var CustomObjectChangedTriggerSchema = TriggerCommonSchema.extend({
696
+ type: import_zod2.z.literal("custom_object_changed"),
697
+ conditions: import_zod2.z.array(import_zod2.z.object({
698
+ operator: import_zod2.z.string(),
699
+ field: import_zod2.z.string().describe("Custom-object field paths use the `customObject.<your_field_name>` prefix. Field names are user-defined per custom object \u2014 query the location's custom objects to discover valid field paths."),
700
+ value: import_zod2.z.unknown().optional(),
701
+ title: import_zod2.z.string().optional(),
702
+ type: import_zod2.z.string().optional(),
703
+ id: import_zod2.z.string().optional()
704
+ }).passthrough()).optional()
705
+ });
706
+ var ConvAiTriggerTriggerSchema = typedTrigger("conv_ai_trigger", [], "fieldless");
707
+ var ConvAiAutonomousTriggerTriggerSchema = typedTrigger("conv_ai_autonomous_trigger", [], "fieldless");
663
708
  var UnknownTriggerSchema = TriggerCommonSchema.extend({
664
709
  type: import_zod2.z.string(),
665
710
  conditions: import_zod2.z.array(import_zod2.z.record(import_zod2.z.unknown())).optional()
@@ -770,6 +815,23 @@ var CreateWorkflowResponseSchema = import_zod3.z.union([
770
815
  version: typeof data.version === "number" ? data.version : 1
771
816
  });
772
817
  });
818
+ function normalizeRemoveFromWorkflowAction(action) {
819
+ if (action.type !== "remove_from_workflow") return action;
820
+ const attrs = action.attributes;
821
+ if (!attrs || typeof attrs !== "object") return action;
822
+ const a = attrs;
823
+ const hasArr = Array.isArray(a.workflow_id) && a.workflow_id.length > 0;
824
+ const hasStr = typeof a.workflowId === "string" && a.workflowId.length > 0;
825
+ if (hasArr && hasStr) return action;
826
+ if (!hasArr && !hasStr) return action;
827
+ const next = { ...a };
828
+ if (hasArr && !hasStr) {
829
+ next.workflowId = a.workflow_id[0];
830
+ } else if (hasStr && !hasArr) {
831
+ next.workflow_id = [a.workflowId];
832
+ }
833
+ return { ...action, attributes: next };
834
+ }
773
835
  function hasId(action) {
774
836
  return typeof action.id === "string" && action.id.length > 0;
775
837
  }
@@ -1074,7 +1136,11 @@ ${errorBody}`
1074
1136
  workflowData: isRecord(raw.workflowData.workflowData) ? raw.workflowData.workflowData : { templates: [] },
1075
1137
  triggers: raw.triggers
1076
1138
  } : raw;
1077
- return WorkflowFullSchema.parse(flat);
1139
+ const parsed = WorkflowFullSchema.parse(flat);
1140
+ if (parsed.workflowData?.templates) {
1141
+ parsed.workflowData.templates = parsed.workflowData.templates.map(normalizeRemoveFromWorkflowAction);
1142
+ }
1143
+ return parsed;
1078
1144
  }
1079
1145
  /**
1080
1146
  * Create a new empty workflow
@@ -1235,7 +1301,7 @@ ${errorBody}`
1235
1301
  */
1236
1302
  buildActionChain(actions) {
1237
1303
  validateActionChain(actions);
1238
- const linked = actions.map((action, i) => {
1304
+ const linked = actions.map(normalizeRemoveFromWorkflowAction).map((action, i) => {
1239
1305
  const copy = { ...action };
1240
1306
  if (!copy.id) {
1241
1307
  copy.id = crypto.randomUUID();
@@ -4308,21 +4374,28 @@ function registerWorkflowBuilderTools(server2, client) {
4308
4374
  );
4309
4375
  server2.tool(
4310
4376
  "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."),
4377
+ "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`. All 10 goal_condition values are catalogued (from GHL's workflow-builder JS bundle, extracted 2026-05-18): email_event, link_click, add_contact_tag, remove_contact_tag, appointment_status, payment_received, form_submission, document_status, invoice_paid, review_request_clicked. Each one has a different `extras` shape (see the `goal_condition` field's description for details).",
4378
+ {
4379
+ goal_condition: import_zod31.z.enum([
4380
+ "email_event",
4381
+ "link_click",
4382
+ "add_contact_tag",
4383
+ "remove_contact_tag",
4384
+ "appointment_status",
4385
+ "payment_received",
4386
+ "form_submission",
4387
+ "document_status",
4388
+ "invoice_paid",
4389
+ "review_request_clicked"
4390
+ ]).describe("The goal-condition identifier. Extras shape per condition: email_event \u2192 {stepIds:[]}; link_click \u2192 {linkIds:[]}; appointment_status \u2192 {calendarId}; payment_received \u2192 {globalProductIds:[]}; form_submission \u2192 {formIds:[]}; document_status \u2192 {templateId}; invoice_paid \u2192 {invoiceStepId}; review_request_clicked \u2192 {reviewTypes:['sms'|'email'], reviewLinkId}; add_contact_tag/remove_contact_tag \u2192 {tags:[]} (uncaptured but inferred)."),
4391
+ extras: import_zod31.z.record(import_zod31.z.unknown()).optional().describe("Goal-condition-specific config. See goal_condition description for the expected shape per value. Pass {} or omit for goal_conditions without extras."),
4392
+ action: import_zod31.z.enum(["exit", "continue", "wait"]).default("exit").describe("What happens when the goal fires (per GHL's GoalAction enum). 'exit' terminates the workflow path (verified). 'continue' lets it proceed to a downstream node. 'wait' pauses on the goal."),
4317
4393
  name: import_zod31.z.string().default("Goal").describe("Display name in the GHL UI."),
4318
4394
  op: import_zod31.z.enum(["or", "and"]).default("or").describe("Top-level boolean operator across segments. Default 'or'."),
4319
4395
  inner_op: import_zod31.z.enum(["or", "and"]).default("or").describe("Boolean operator across conditions within a single segment. Default 'or'.")
4320
4396
  },
4321
- async ({ goal_condition, extras, action, target_node_id, name, op, inner_op }) => {
4397
+ async ({ goal_condition, extras, action, name, op, inner_op }) => {
4322
4398
  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
4399
  const nodeId = crypto.randomUUID();
4327
4400
  const conditionId = crypto.randomUUID();
4328
4401
  const attributes = {
@@ -4338,9 +4411,6 @@ function registerWorkflowBuilderTools(server2, client) {
4338
4411
  type: "workflow_goal",
4339
4412
  action
4340
4413
  };
4341
- if (action === "goto" && target_node_id) {
4342
- attributes.targetNodeId = target_node_id;
4343
- }
4344
4414
  const goalNode = {
4345
4415
  id: nodeId,
4346
4416
  name,
@@ -6724,7 +6794,7 @@ async function validateFirebase(firebaseKey, refreshToken) {
6724
6794
  function registerSetupTool(server2) {
6725
6795
  server2.tool(
6726
6796
  "setup_ghl_mcp",
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).",
6797
+ "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 (148 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
6728
6798
  {
6729
6799
  email: import_zod42.z.string().email().describe("Email used at purchase."),
6730
6800
  license_key: import_zod42.z.string().min(20).describe("License key from your purchase email."),
@@ -6779,7 +6849,7 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
6779
6849
  ghl_firebase_api_key: workflowBuilderEnabled ? args.ghl_firebase_api_key?.trim() : void 0,
6780
6850
  ghl_firebase_refresh_token: workflowBuilderEnabled ? args.ghl_firebase_refresh_token?.trim() : void 0
6781
6851
  });
6782
- const toolCount = workflowBuilderEnabled ? "178" : "168";
6852
+ const toolCount = workflowBuilderEnabled ? "178" : "148";
6783
6853
  const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
6784
6854
  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.";
6785
6855
  return {
@@ -6807,7 +6877,7 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
6807
6877
  function registerEnableWorkflowBuilderTool(server2) {
6808
6878
  server2.tool(
6809
6879
  "enable_workflow_builder",
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.",
6880
+ "Add Firebase credentials to an existing GHL Command install to unlock 30 additional tools across 6 modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder (10 tools), form builder (5 tools), pipeline builder (5 tools), and workflow cloning. 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 148 to 178 after the next Claude restart.",
6811
6881
  {
6812
6882
  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."),
6813
6883
  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."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
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": {