@elitedcs/ghl-mcp 3.41.0 → 3.43.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.
Files changed (2) hide show
  1. package/dist/index.js +226 -17
  2. package/package.json +1 -1
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.41.0",
34
+ version: "3.43.0",
35
35
  mcpName: "io.github.drjerryrelth/ghl-command",
36
36
  description: "GoHighLevel MCP Server for Claude. 218 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
37
37
  main: "dist/index.js",
@@ -10704,9 +10704,21 @@ var pageSchema = import_zod52.z.object({
10704
10704
  formRef: nsRef("form").optional(),
10705
10705
  calendarRef: nsRef("calendar").optional()
10706
10706
  });
10707
+ var FUNNEL_TARGETS = ["ghl", "external"];
10708
+ var FUNNEL_HOSTS = ["cloudflare", "vercel"];
10707
10709
  var funnelSchema = import_zod52.z.object({
10708
10710
  ref: nsRef("funnel"),
10709
10711
  name: import_zod52.z.string(),
10712
+ // Where the funnel is built. "ghl" (default) = funnel + named steps in GHL.
10713
+ // "external" = the subscriber builds + hosts the site themselves (Cloudflare/
10714
+ // Vercel) and wires its form back to this GHL sub-account (POWER-USER path —
10715
+ // see blueprint-funnel-targets-spec.md §9). The executor does NOT build or
10716
+ // deploy an external funnel; it surfaces the GHL-side wiring info.
10717
+ target: import_zod52.z.enum(FUNNEL_TARGETS).optional(),
10718
+ host: import_zod52.z.enum(FUNNEL_HOSTS).optional(),
10719
+ // external only
10720
+ domain: import_zod52.z.string().optional(),
10721
+ // external only
10710
10722
  pages: import_zod52.z.array(pageSchema)
10711
10723
  });
10712
10724
  var emailAssetSchema = import_zod52.z.object({
@@ -11006,6 +11018,14 @@ function validateBuildPlan(input) {
11006
11018
  }
11007
11019
  }
11008
11020
  }
11021
+ for (const fn of plan.funnels ?? []) {
11022
+ const isExternal = fn.target === "external";
11023
+ if (!isExternal && (fn.host !== void 0 || fn.domain !== void 0)) {
11024
+ warnings.push(
11025
+ `funnel "${fn.ref}" sets host/domain but target is not "external" \u2014 those are ignored for a GHL-built funnel`
11026
+ );
11027
+ }
11028
+ }
11009
11029
  for (const p of plan.pipelines ?? []) {
11010
11030
  const positions = p.stages.map((s) => s.position).sort((a, b) => a - b);
11011
11031
  const expected = positions.every((pos, idx) => pos === idx);
@@ -11089,7 +11109,9 @@ function buildRefIndex(plan) {
11089
11109
  sms: /* @__PURE__ */ new Map(),
11090
11110
  workflowName: /* @__PURE__ */ new Map(),
11091
11111
  pipelineName: /* @__PURE__ */ new Map(),
11092
- stageName: /* @__PURE__ */ new Map()
11112
+ stageName: /* @__PURE__ */ new Map(),
11113
+ formName: /* @__PURE__ */ new Map(),
11114
+ calendarName: /* @__PURE__ */ new Map()
11093
11115
  };
11094
11116
  for (const t of plan.tags ?? []) idx.tagName.set(t.ref, t.name);
11095
11117
  for (const f of plan.customFields ?? []) {
@@ -11103,6 +11125,8 @@ function buildRefIndex(plan) {
11103
11125
  idx.pipelineName.set(p.ref, p.name);
11104
11126
  for (const st of p.stages) idx.stageName.set(st.ref, st.name);
11105
11127
  }
11128
+ for (const fm of plan.forms ?? []) idx.formName.set(fm.ref, fm.name);
11129
+ for (const c of plan.calendars ?? []) idx.calendarName.set(c.ref, c.name);
11106
11130
  return idx;
11107
11131
  }
11108
11132
  function htmlWrap(text) {
@@ -11284,13 +11308,31 @@ function expandAction(action, idx, idMap) {
11284
11308
  }
11285
11309
  case "create_opportunity":
11286
11310
  case "update_opportunity": {
11311
+ const pipelineId = resolveId(action.pipelineRef, idMap);
11312
+ const stageId = resolveId(action.stageRef, idMap);
11313
+ const pendingRefs = [];
11314
+ if (isPending(pipelineId)) pendingRefs.push(action.pipelineRef);
11315
+ if (isPending(stageId)) pendingRefs.push(action.stageRef);
11287
11316
  const pName = idx.pipelineName.get(action.pipelineRef) ?? action.pipelineRef;
11288
11317
  const sName = idx.stageName.get(action.stageRef) ?? action.stageRef;
11289
- const verb = action.type === "create_opportunity" ? "Create" : "Move";
11318
+ const verb = action.type === "create_opportunity" ? "Create opportunity" : "Move opportunity";
11290
11319
  return {
11291
- kind: "manual",
11292
- logicalType: action.type,
11293
- reason: `${verb} opportunity \u2192 "${pName}" / stage "${sName}": GHL rejects a synthesized opportunity node ("corrupted type") and silently kills the whole workflow save. Add this step by hand in the GHL workflow builder (Add action \u2192 Create/Update Opportunity), or round-trip an existing node.`
11320
+ kind: "expanded",
11321
+ pendingRefs,
11322
+ native: {
11323
+ type: "internal_update_opportunity",
11324
+ name: `${verb}: ${pName} / ${sName}`,
11325
+ // NODE-level discriminator (the normalizer also forces this; included so
11326
+ // the expanded action is correct even outside the save path).
11327
+ workflowsActionType: "INTERNAL",
11328
+ attributes: {
11329
+ type: "internal_update_opportunity",
11330
+ __customInputFields__: [
11331
+ { filterField: "pipelineId", value: pipelineId, valueFieldType: "select", dataType: "SINGLE_OPTIONS" },
11332
+ { filterField: "pipelineStageId", value: stageId, valueFieldType: "select", dataType: "SINGLE_OPTIONS" }
11333
+ ]
11334
+ }
11335
+ }
11294
11336
  };
11295
11337
  }
11296
11338
  case "goal_event": {
@@ -11316,6 +11358,42 @@ function expandAction(action, idx, idMap) {
11316
11358
  }
11317
11359
  }
11318
11360
  var MAX_ACTIONS_PER_WORKFLOW = 40;
11361
+ function triggerPlainEnglish(trigger, idx) {
11362
+ const formName = trigger.formRef ? idx.formName.get(trigger.formRef) ?? trigger.formRef : "?";
11363
+ const stageName = trigger.stageRef ? idx.stageName.get(trigger.stageRef) ?? trigger.stageRef : "?";
11364
+ const calName = trigger.calendarRef ? idx.calendarName.get(trigger.calendarRef) ?? trigger.calendarRef : "?";
11365
+ switch (trigger.type) {
11366
+ case "form_submission":
11367
+ case "form_submitted":
11368
+ return `when form "${formName}" is submitted`;
11369
+ case "pipeline_stage_updated":
11370
+ return `when an opportunity reaches stage "${stageName}"`;
11371
+ case "appointment":
11372
+ return `on an appointment event${trigger.calendarRef ? ` for calendar "${calName}"` : ""}`;
11373
+ case "customer_reply":
11374
+ return "when a contact replies";
11375
+ default:
11376
+ return `trigger type "${trigger.type}"`;
11377
+ }
11378
+ }
11379
+ function expandTrigger(trigger, idx) {
11380
+ if (trigger.type === "contact_tag") {
11381
+ if (!trigger.tagRef) return { kind: "manual", reason: "Set this workflow's contact-tag trigger in the GHL UI (the plan trigger has no tag)." };
11382
+ const tagName = idx.tagName.get(trigger.tagRef) ?? trigger.tagRef;
11383
+ return {
11384
+ kind: "native",
11385
+ trigger: {
11386
+ name: "Contact Tag",
11387
+ type: "contact_tag",
11388
+ conditions: [{ operator: "index-of-true", field: "tagsAdded", value: tagName, title: "Tag Added", type: "select", id: "tag-added" }]
11389
+ }
11390
+ };
11391
+ }
11392
+ return {
11393
+ kind: "manual",
11394
+ reason: `Set this workflow's trigger in the GHL UI: ${triggerPlainEnglish(trigger, idx)}, then publish. (Blueprint builds the workflow + all its steps as DRAFT; only this trigger type isn't auto-built yet.)`
11395
+ };
11396
+ }
11319
11397
  function expandWorkflow(workflow, idx, idMap, gatedBy) {
11320
11398
  const nativeActions = [];
11321
11399
  const manual = [];
@@ -11333,6 +11411,13 @@ function expandWorkflow(workflow, idx, idMap, gatedBy) {
11333
11411
  }
11334
11412
  });
11335
11413
  const splitInto = Math.max(1, Math.ceil(nativeActions.length / MAX_ACTIONS_PER_WORKFLOW));
11414
+ let nativeTrigger;
11415
+ let triggerManual;
11416
+ if (workflow.trigger) {
11417
+ const t = expandTrigger(workflow.trigger, idx);
11418
+ if (t.kind === "native") nativeTrigger = t.trigger;
11419
+ else triggerManual = t.reason;
11420
+ }
11336
11421
  return {
11337
11422
  ref: workflow.ref,
11338
11423
  name: workflow.name,
@@ -11341,7 +11426,9 @@ function expandWorkflow(workflow, idx, idMap, gatedBy) {
11341
11426
  needsContent,
11342
11427
  pendingRefs: [...pendingRefs],
11343
11428
  splitInto,
11344
- gatedBy
11429
+ gatedBy,
11430
+ nativeTrigger,
11431
+ triggerManual
11345
11432
  };
11346
11433
  }
11347
11434
  function scanSection(section2, planObjects, existing) {
@@ -11379,7 +11466,10 @@ var SECTION_OBJECTS = {
11379
11466
  customValues: (p) => (p.customValues ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11380
11467
  calendars: (p) => (p.calendars ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11381
11468
  forms: (p) => (p.forms ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11382
- funnels: (p) => (p.funnels ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11469
+ // EXTERNAL funnels are NOT built in GHL (the user hosts them) exclude them
11470
+ // from the scan so dry-run matches execute: they aren't counted as would-create,
11471
+ // don't seed idMap, and can't trip onConflict on a GHL-funnel name collision.
11472
+ funnels: (p) => (p.funnels ?? []).filter((x) => x.target !== "external").map((x) => ({ ref: x.ref, name: x.name })),
11383
11473
  emails: (p) => (p.emails ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11384
11474
  sms: (p) => (p.sms ?? []).map((x) => ({ ref: x.ref, name: x.name })),
11385
11475
  workflows: (p) => (p.workflows ?? []).map((x) => ({ ref: x.ref, name: x.name }))
@@ -11467,12 +11557,13 @@ function renderReport(plan, result, ctx) {
11467
11557
  );
11468
11558
  L.push("");
11469
11559
  const manualCalRefs = new Set(result.calendarsManual.map((c) => c.ref));
11560
+ const externalFunnelRefs = new Set((plan.funnels ?? []).filter((f) => f.target === "external").map((f) => f.ref));
11470
11561
  L.push("\u2500\u2500 Blueprint builds automatically \u2500\u2500");
11471
11562
  for (const section2 of EXECUTION_ORDER) {
11472
11563
  const secItems = result.items.filter((i) => i.type === section2);
11473
11564
  if (secItems.length === 0) continue;
11474
11565
  for (const it of secItems) {
11475
- if (manualCalRefs.has(it.ref)) continue;
11566
+ if (manualCalRefs.has(it.ref) || externalFunnelRefs.has(it.ref)) continue;
11476
11567
  const mark = it.status === "existing" ? "skip (exists)" : ctx.mode === "dry_run" ? "would create" : "create";
11477
11568
  L.push(` [${section2}] ${it.name} \u2014 ${mark}${it.existingId ? ` \u2192 ${it.existingId}` : ""}`);
11478
11569
  }
@@ -11491,9 +11582,17 @@ function renderReport(plan, result, ctx) {
11491
11582
  }
11492
11583
  for (const fn of plan.funnels ?? []) {
11493
11584
  any = true;
11494
- L.push(` \u2022 [funnel] Design + populate the pages of funnel "${fn.name}" (Blueprint builds the funnel + steps; page content/design is manual).`);
11585
+ if (fn.target === "external") {
11586
+ L.push(` \u2022 [funnel] "${fn.name}" is EXTERNAL (host: ${fn.host ?? "cloudflare"}) \u2014 build + host the site yourself and wire its form to this GHL sub-account (contacts API + the workflow trigger tag); booking \u2192 the GHL calendar URL. Power-user path.`);
11587
+ } else {
11588
+ L.push(` \u2022 [funnel] Design + populate the pages of funnel "${fn.name}" (Blueprint builds the funnel + steps; page content/design is manual).`);
11589
+ }
11495
11590
  }
11496
11591
  for (const w of result.workflows) {
11592
+ if (w.triggerManual) {
11593
+ any = true;
11594
+ L.push(` \u2022 [${w.name}] ${w.triggerManual}`);
11595
+ }
11497
11596
  for (const m of w.manual) {
11498
11597
  any = true;
11499
11598
  L.push(` \u2022 [${w.name}] ${m.reason}`);
@@ -11737,7 +11836,21 @@ async function executeBackbone(plan, deps, opts = {}) {
11737
11836
  });
11738
11837
  return `Design + publish the pages of funnel "${fn.name}": ${pageBits.join("; ")}. (Blueprint built the funnel + steps; page content/design is manual.)`;
11739
11838
  };
11839
+ const externalFunnelReason = (fn) => {
11840
+ const host = fn.host ?? "cloudflare";
11841
+ const bits = fn.pages.map((pg) => {
11842
+ const hosts = [];
11843
+ if (pg.formRef) hosts.push(`form "${formNameByRef.get(pg.formRef) ?? pg.formRef}"`);
11844
+ if (pg.calendarRef) hosts.push(`booking \u2192 calendar "${calNameByRef.get(pg.calendarRef) ?? pg.calendarRef}"`);
11845
+ return `page "${pg.name}"${hosts.length ? ` (${hosts.join(", ")})` : ""}`;
11846
+ });
11847
+ return `Funnel "${fn.name}" is EXTERNAL (host: ${host}${fn.domain ? `, domain: ${fn.domain}` : ""}) \u2014 Blueprint does NOT build or deploy it. Build + host the site yourself (technically-capable path), then wire its form to THIS GHL sub-account: upsert the contact via the GHL contacts API (your Private Integration token as a host-side secret) with the plan's custom fields, then add the tag that triggers your speed-to-lead workflow; ${bits.join("; ")}.`;
11848
+ };
11740
11849
  for (const fn of plan.funnels ?? []) {
11850
+ if (fn.target === "external") {
11851
+ manual.push({ ref: fn.ref, type: "funnel-external", name: fn.name, reason: externalFunnelReason(fn) });
11852
+ continue;
11853
+ }
11741
11854
  let funnels;
11742
11855
  try {
11743
11856
  funnels = await deps.listFunnels();
@@ -11805,6 +11918,73 @@ async function executeBackbone(plan, deps, opts = {}) {
11805
11918
  }
11806
11919
  manual.push({ ref: fn.ref, type: "funnel-page-content", name: fn.name, reason: funnelContentReason(fn) });
11807
11920
  }
11921
+ const wfIndex = buildRefIndex(plan);
11922
+ const wfIdMap = new Map(Object.entries(idMap));
11923
+ for (const wf of plan.workflows ?? []) {
11924
+ let workflows;
11925
+ try {
11926
+ workflows = await deps.listWorkflows();
11927
+ } catch (e) {
11928
+ return halt(wf.ref, "workflow", `could not read existing workflows: ${msg(e)}`);
11929
+ }
11930
+ const matches = workflows.filter((w) => norm2(w.name) === norm2(wf.name));
11931
+ if (matches.length > 1) {
11932
+ return halt(wf.ref, "workflow", `${matches.length} existing workflows are named "${wf.name}" \u2014 ambiguous, cannot safely bind ${wf.ref}. Resolve the duplicate in GHL, then re-run.`);
11933
+ }
11934
+ const expansion = expandWorkflow(wf, wfIndex, wfIdMap, []);
11935
+ if (expansion.pendingRefs.length) {
11936
+ return halt(wf.ref, "workflow", `workflow "${wf.name}" references objects that weren't built/resolved: ${expansion.pendingRefs.join(", ")}. Cannot build it.`);
11937
+ }
11938
+ if (matches.length === 1) {
11939
+ const exId = matches[0].id;
11940
+ if (expansion.nativeActions.length > 0) {
11941
+ let existingCount = -1;
11942
+ try {
11943
+ existingCount = await deps.getWorkflowActionCount(exId);
11944
+ } catch {
11945
+ existingCount = -1;
11946
+ }
11947
+ if (existingCount === 0) {
11948
+ return halt(wf.ref, "workflow", `a workflow named "${wf.name}" already exists but has NO actions \u2014 likely an empty orphan from a failed prior run (or a name collision). Delete it in GHL, then re-run. (Blueprint never modifies an existing workflow.)`);
11949
+ }
11950
+ }
11951
+ idMap[wf.ref] = exId;
11952
+ built.push({ ref: wf.ref, type: "workflow", name: wf.name, status: "existing", realId: exId });
11953
+ continue;
11954
+ }
11955
+ let wfId;
11956
+ try {
11957
+ wfId = await deps.createWorkflow(wf.name);
11958
+ } catch (e) {
11959
+ return halt(wf.ref, "workflow", `create failed: ${msg(e)}`);
11960
+ }
11961
+ if (!wfId) return halt(wf.ref, "workflow", "workflow create returned no id");
11962
+ const triggers = expansion.nativeTrigger ? [expansion.nativeTrigger] : [];
11963
+ try {
11964
+ await deps.saveWorkflow(wfId, { actions: expansion.nativeActions, triggers, stopOnResponse: wf.stopOnResponse });
11965
+ if (expansion.nativeActions.length > 0) {
11966
+ const count = await deps.getWorkflowActionCount(wfId);
11967
+ if (count === 0) throw new Error("saved but no actions persisted (read-after-write)");
11968
+ }
11969
+ } catch (e) {
11970
+ let rolledBack = false;
11971
+ try {
11972
+ await deps.deleteWorkflow(wfId);
11973
+ rolledBack = true;
11974
+ } catch {
11975
+ }
11976
+ return halt(
11977
+ wf.ref,
11978
+ "workflow",
11979
+ rolledBack ? `actions/trigger save failed; the partial workflow was rolled back (deleted): ${msg(e)}` : `actions/trigger save failed AND the rollback delete also failed \u2014 manually delete the orphan workflow (id ${wfId}) in GHL before re-running: ${msg(e)}`
11980
+ );
11981
+ }
11982
+ idMap[wf.ref] = wfId;
11983
+ built.push({ ref: wf.ref, type: "workflow", name: wf.name, status: "created", realId: wfId });
11984
+ if (expansion.triggerManual) manual.push({ ref: wf.ref, type: "workflow-trigger", name: wf.name, reason: `[${wf.name}] ${expansion.triggerManual}` });
11985
+ for (const nc of expansion.needsContent) manual.push({ ref: wf.ref, type: "workflow-action", name: wf.name, reason: `[${wf.name}] ${nc.reason}` });
11986
+ for (const m of expansion.manual) manual.push({ ref: wf.ref, type: "workflow-action", name: wf.name, reason: `[${wf.name}] ${m.reason}` });
11987
+ }
11808
11988
  return { ok: true, idMap, built, manual, deferred: deferredSections(plan) };
11809
11989
  }
11810
11990
  async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMap, built) {
@@ -11839,10 +12019,8 @@ async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMa
11839
12019
  }
11840
12020
  return null;
11841
12021
  }
11842
- function deferredSections(plan) {
11843
- const out = [];
11844
- if (plan.workflows?.length) out.push({ section: "workflows", count: plan.workflows.length });
11845
- return out;
12022
+ function deferredSections(_plan) {
12023
+ return [];
11846
12024
  }
11847
12025
  function msg(e) {
11848
12026
  return e instanceof Error ? e.message : String(e);
@@ -12192,6 +12370,35 @@ ${text2.slice(0, 300)}`);
12192
12370
  },
12193
12371
  deleteFunnel: async (funnelId) => {
12194
12372
  await funnelApi("POST", `/funnel/delete`, { funnelId, locationId: locationId2, userId: builderClient.getUserId() });
12373
+ },
12374
+ // ── Workflows ───────────────────────────────────────────────────────────
12375
+ // NOTE: unlike /forms/ and /funnels/, the GHL /workflows/ list endpoint does
12376
+ // NOT paginate — it 422s on `limit`/`skip` ("property X should not exist") and
12377
+ // returns ALL workflows in one call (live-verified 2026-06-16). So a single
12378
+ // call IS the full never-clobber scan here; no pagination is possible or needed.
12379
+ listWorkflows: async () => pickObjects(await client.get("/workflows/", { params: { locationId: locationId2 } }), ["workflows"]),
12380
+ createWorkflow: async (name) => {
12381
+ const wf = await builderClient.createWorkflow(name);
12382
+ const id = wf && typeof wf === "object" ? wf : {};
12383
+ const wfId = typeof id.id === "string" ? id.id : typeof id._id === "string" ? id._id : void 0;
12384
+ if (!wfId) throw new Error(`create_workflow returned no id: ${JSON.stringify(wf).slice(0, 200)}`);
12385
+ return wfId;
12386
+ },
12387
+ saveWorkflow: async (workflowId, opts) => {
12388
+ await builderClient.updateWorkflow(workflowId, {
12389
+ actions: opts.actions,
12390
+ triggers: opts.triggers,
12391
+ status: "draft",
12392
+ stopOnResponse: opts.stopOnResponse
12393
+ });
12394
+ },
12395
+ getWorkflowActionCount: async (workflowId) => {
12396
+ const full = await builderClient.getWorkflow(workflowId);
12397
+ const templates = full && typeof full === "object" ? full.workflowData : void 0;
12398
+ return Array.isArray(templates?.templates) ? templates.templates.length : 0;
12399
+ },
12400
+ deleteWorkflow: async (workflowId) => {
12401
+ await builderClient.deleteWorkflow(workflowId);
12195
12402
  }
12196
12403
  };
12197
12404
  }
@@ -12250,7 +12457,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12250
12457
  );
12251
12458
  server2.tool(
12252
12459
  "apply_build_plan",
12253
- `Take an APPROVED Blueprint \xA75 build plan + the CURRENT confirmed sub-account and build it. mode:"dry_run" (default) writes NOTHING \u2014 it resolves refs, expands each workflow's logical actions to native GHL JSON, runs the NEVER-CLOBBER existing-asset scan, and returns a two-part report. Run it FIRST. mode:"execute" performs LIVE writes for the CRM backbone (pipelines+stages, custom fields, tags, custom values), calendars, AND forms: never clobbers (same-named objects are bound to their existing id, never modified), verifies each create by read-back before resolving its ref, halts on the first failure returning the partial idMap, and is idempotent (re-run = no-op). Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole account user (auto-assigns you as the team member); with 0 or 2+ users they're surfaced as a manual step, not auto-staffed to a guess. Forms build with their standard + custom fields (custom fieldRefs resolve to the real fields created earlier in the run). Funnels build structurally (funnel + named steps), with each funnel emitting a manual 'design + populate the pages' step (page content/HTML is not auto-built \u2014 plans carry outlines). Workflows are surfaced as manual next steps, not auto-built yet. Always confirms the active location and validates the plan before any write.`,
12460
+ `Take an APPROVED Blueprint \xA75 build plan + the CURRENT confirmed sub-account and build it. mode:"dry_run" (default) writes NOTHING \u2014 it resolves refs, expands each workflow's logical actions to native GHL JSON, runs the NEVER-CLOBBER existing-asset scan, and returns a two-part report. Run it FIRST. mode:"execute" performs LIVE writes for the CRM backbone (pipelines+stages, custom fields, tags, custom values), calendars, AND forms: never clobbers (same-named objects are bound to their existing id, never modified), verifies each create by read-back before resolving its ref, halts on the first failure returning the partial idMap, and is idempotent (re-run = no-op). Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole account user (auto-assigns you as the team member); with 0 or 2+ users they're surfaced as a manual step, not auto-staffed to a guess. Forms build with their standard + custom fields (custom fieldRefs resolve to the real fields created earlier in the run). Funnels: a GHL funnel (target:"ghl", default) builds structurally (funnel + named steps; page content/HTML is a manual step \u2014 plans carry outlines); a funnel with target:"external" is NOT built or deployed here \u2014 the user builds + hosts the site themselves (Cloudflare/Vercel) and wires its form back to this GHL sub-account (surfaced as a manual wiring step). Workflows build as DRAFT with all their logical actions expanded to native GHL JSON (incl. opportunity create/move steps, re-enabled v3.41.0) and chained; a contact_tag trigger is built automatically, other trigger types are surfaced as a manual step; the operator reviews + publishes. Always confirms the active location and validates the plan before any write.`,
12254
12461
  {
12255
12462
  plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The approved \xA75 Build Plan object."),
12256
12463
  mode: import_zod53.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
@@ -12343,7 +12550,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12343
12550
  manual: exec.manual,
12344
12551
  idMap: exec.idMap,
12345
12552
  deferred: exec.deferred,
12346
- deferredNote: "execute builds the CRM backbone (pipelines, custom fields, tags, custom values), calendars, forms, and funnels (funnel + named steps) live. Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole user, else manual. Funnel PAGE CONTENT/design is not auto-built (plans carry outlines, not HTML) \u2014 each funnel lists a manual 'design + populate the pages' step. Workflows are planned but NOT auto-built yet.",
12553
+ deferredNote: 'execute builds the WHOLE plan live: CRM backbone (pipelines, custom fields, tags, custom values), calendars, forms, funnels (GHL-built = funnel + named steps), and workflows (DRAFT, with all steps incl. opportunity create/move). Remaining manual steps are surfaced per item: staff-requiring calendars in multi-user accounts, GHL funnel page content/design, EXTERNAL funnels (target:"external" \u2014 the user builds + hosts the site themselves and wires it back; not built here), workflow triggers other than contact_tag, and publishing the DRAFT workflows.',
12347
12554
  nextManualSteps: [...execManualLines, ...manualLines, ...handoffLines],
12348
12555
  summary: exec.ok ? `Built ${exec.built.filter((b) => b.status === "created").length} new object(s), bound ${exec.built.filter((b) => b.status === "existing").length} existing.${exec.manual.length ? ` ${exec.manual.length} item(s) need a manual step (see nextManualSteps).` : ""}${exec.deferred.length ? " Deferred: " + exec.deferred.map((d) => `${d.count} ${d.section}`).join(", ") + " (manual)." : ""}` : `HALTED at ${exec.halted?.atRef} (${exec.halted?.reason}). ${exec.built.length} object(s) were created before the halt \u2014 see idMap to resume or clean up. NEVER-CLOBBER means a re-run will bind those, not duplicate them.`
12349
12556
  });
@@ -12375,6 +12582,8 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12375
12582
  autoActions: w.nativeActions.length,
12376
12583
  manualSteps: w.manual,
12377
12584
  needsContent: w.needsContent,
12585
+ triggerManual: w.triggerManual,
12586
+ triggerAutoBuilt: !!w.nativeTrigger,
12378
12587
  pendingRefs: w.pendingRefs,
12379
12588
  splitInto: w.splitInto,
12380
12589
  gatedBy: w.gatedBy,
@@ -12383,7 +12592,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
12383
12592
  calendarsManual: result.calendarsManual,
12384
12593
  handoffs: result.handoffs,
12385
12594
  report,
12386
- next: 'Review the report. When it looks right, re-run with mode:"execute" to build live: the CRM backbone (pipelines, fields, tags, custom values), calendars, forms, and funnels (funnel + named steps). Funnel page content/design and workflows are listed as manual next steps.'
12595
+ next: `Review the report. When it looks right, re-run with mode:"execute" to build it all live: CRM backbone (pipelines, fields, tags, custom values), calendars, forms, GHL funnels (funnel + named steps), and workflows (DRAFT, with all steps incl. opportunity moves). Manual next steps (see the report's Part 2): GHL funnel page content/design; EXTERNAL funnels (target:"external") which you build + host yourself and wire back; workflow triggers other than contact_tag; then publish the workflows.`
12387
12596
  });
12388
12597
  } catch (error) {
12389
12598
  return errorResponse(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.41.0",
3
+ "version": "3.43.0",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
5
  "description": "GoHighLevel MCP Server for Claude. 218 tools — full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",