@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.
- package/dist/index.js +226 -17
- 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.
|
|
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: "
|
|
11292
|
-
|
|
11293
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
11843
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
|
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",
|