@elitedcs/ghl-mcp 3.39.0 → 3.41.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 +256 -23
- package/package.json +1 -1
- package/templates/action-schemas.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.41.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",
|
|
@@ -705,6 +705,12 @@ function decodeFirebaseClaims(idToken) {
|
|
|
705
705
|
}
|
|
706
706
|
}
|
|
707
707
|
|
|
708
|
+
// src/id-shape.ts
|
|
709
|
+
var GHL_ID_SHAPE = /^([A-Za-z0-9]{17,}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
|
|
710
|
+
function isIdShaped(s) {
|
|
711
|
+
return typeof s === "string" && GHL_ID_SHAPE.test(s);
|
|
712
|
+
}
|
|
713
|
+
|
|
708
714
|
// src/trigger-schemas.ts
|
|
709
715
|
var import_zod3 = require("zod");
|
|
710
716
|
var TriggerActionSchema = import_zod3.z.object({
|
|
@@ -1170,6 +1176,31 @@ function normalizeRemoveFromWorkflowAction(action) {
|
|
|
1170
1176
|
}
|
|
1171
1177
|
return { ...action, attributes: next };
|
|
1172
1178
|
}
|
|
1179
|
+
function normalizeInternalUpdateOpportunityAction(action) {
|
|
1180
|
+
if (action.type !== "internal_update_opportunity") return action;
|
|
1181
|
+
const attrIn = action.attributes && typeof action.attributes === "object" ? action.attributes : {};
|
|
1182
|
+
const fieldsIn = Array.isArray(attrIn.__customInputFields__) ? attrIn.__customInputFields__ : [];
|
|
1183
|
+
const __customInputFields__ = fieldsIn.map((raw) => {
|
|
1184
|
+
const f = raw && typeof raw === "object" ? raw : {};
|
|
1185
|
+
return {
|
|
1186
|
+
...f,
|
|
1187
|
+
// shape-preserving — keep every key a real node carries
|
|
1188
|
+
__customInputs__: f.__customInputs__ && typeof f.__customInputs__ === "object" ? f.__customInputs__ : {},
|
|
1189
|
+
dataType: typeof f.dataType === "string" ? f.dataType : "SINGLE_OPTIONS",
|
|
1190
|
+
valueFieldType: typeof f.valueFieldType === "string" ? f.valueFieldType : "select"
|
|
1191
|
+
};
|
|
1192
|
+
});
|
|
1193
|
+
const attributes = {
|
|
1194
|
+
...attrIn,
|
|
1195
|
+
// shape-preserving
|
|
1196
|
+
type: "internal_update_opportunity",
|
|
1197
|
+
allowBackward: typeof attrIn.allowBackward === "boolean" ? attrIn.allowBackward : false,
|
|
1198
|
+
__customInputs__: attrIn.__customInputs__ && typeof attrIn.__customInputs__ === "object" ? attrIn.__customInputs__ : {},
|
|
1199
|
+
__customInputFields__
|
|
1200
|
+
};
|
|
1201
|
+
delete attributes.workflowsActionType;
|
|
1202
|
+
return { ...action, workflowsActionType: "INTERNAL", attributes };
|
|
1203
|
+
}
|
|
1173
1204
|
function hasId(action) {
|
|
1174
1205
|
return typeof action.id === "string" && action.id.length > 0;
|
|
1175
1206
|
}
|
|
@@ -1185,7 +1216,7 @@ function getStringArray(value) {
|
|
|
1185
1216
|
}
|
|
1186
1217
|
return value;
|
|
1187
1218
|
}
|
|
1188
|
-
function validateActionChain(actions) {
|
|
1219
|
+
function validateActionChain(actions, existingIds) {
|
|
1189
1220
|
const byId = new Map(actions.filter(hasId).map((action) => [action.id, action]));
|
|
1190
1221
|
for (const action of actions) {
|
|
1191
1222
|
const attr = isRecord(action.attributes) ? action.attributes : void 0;
|
|
@@ -1207,12 +1238,24 @@ function validateActionChain(actions) {
|
|
|
1207
1238
|
if (!attr.startAfter) throw new Error(`Wait action "${action.name}" missing required 'startAfter' in attributes.`);
|
|
1208
1239
|
break;
|
|
1209
1240
|
case "internal_update_opportunity": {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
)
|
|
1241
|
+
const isRoundTripped = hasId(action) && (existingIds ? existingIds.has(action.id) : true);
|
|
1242
|
+
if (!isRoundTripped) {
|
|
1243
|
+
const cif = Array.isArray(attr.__customInputFields__) ? attr.__customInputFields__ : null;
|
|
1244
|
+
if (!cif) {
|
|
1245
|
+
throw new Error(
|
|
1246
|
+
`Internal update opportunity action "${action.name}" missing '__customInputFields__' array (needs a pipelineId entry and a pipelineStageId entry). Use get_pipelines to find the IDs.`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
const target = (ff) => cif.find((f) => f && f.filterField === ff);
|
|
1250
|
+
for (const ff of ["pipelineId", "pipelineStageId"]) {
|
|
1251
|
+
const t = target(ff);
|
|
1252
|
+
if (!t || !isIdShaped(t.value)) {
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
`Internal update opportunity action "${action.name}" needs a valid ${ff} entry (an id, not a name) in '__customInputFields__'. Use get_pipelines / list_pipelines_full to find the IDs. (A missing or non-existent id makes GHL silently fail this action and can kill the rest.)`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1214
1258
|
}
|
|
1215
|
-
if (!Array.isArray(attr.__customInputFields__)) throw new Error(`Internal update opportunity action "${action.name}" missing '__customInputFields__' array.`);
|
|
1216
1259
|
break;
|
|
1217
1260
|
}
|
|
1218
1261
|
case "remove_from_workflow":
|
|
@@ -1686,7 +1729,8 @@ ${errorBody}`
|
|
|
1686
1729
|
}
|
|
1687
1730
|
const currentActions = current.workflowData?.templates || [];
|
|
1688
1731
|
const newActions = updates.actions ?? currentActions;
|
|
1689
|
-
const
|
|
1732
|
+
const existingActionIds = new Set(currentActions.filter(hasId).map((a) => a.id));
|
|
1733
|
+
const linkedActions = this.buildActionChain(newActions, existingActionIds);
|
|
1690
1734
|
const currentIds = new Set(currentActions.filter(hasId).map((a) => a.id));
|
|
1691
1735
|
const newIds = new Set(linkedActions.filter(hasId).map((a) => a.id));
|
|
1692
1736
|
const createdSteps = linkedActions.filter(hasId).filter((a) => !currentIds.has(a.id)).map((a) => a.id);
|
|
@@ -1831,9 +1875,9 @@ ${errorBody}`
|
|
|
1831
1875
|
* saves correctly but subsequent actions get skipped during execution.
|
|
1832
1876
|
* Only use arrays for branching nodes (if/else) that point to multiple targets.
|
|
1833
1877
|
*/
|
|
1834
|
-
buildActionChain(actions) {
|
|
1835
|
-
validateActionChain(actions);
|
|
1836
|
-
const linked = actions.map(normalizeRemoveFromWorkflowAction).map((action, i) => {
|
|
1878
|
+
buildActionChain(actions, existingIds) {
|
|
1879
|
+
validateActionChain(actions, existingIds);
|
|
1880
|
+
const linked = actions.map(normalizeRemoveFromWorkflowAction).map(normalizeInternalUpdateOpportunityAction).map((action, i) => {
|
|
1837
1881
|
const copy = { ...action };
|
|
1838
1882
|
if (!copy.id) {
|
|
1839
1883
|
copy.id = crypto.randomUUID();
|
|
@@ -5512,7 +5556,7 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
5512
5556
|
);
|
|
5513
5557
|
server2.tool(
|
|
5514
5558
|
"update_workflow_actions",
|
|
5515
|
-
"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. NOTE: internal_update_opportunity (GHL's 'Update Opportunity' / move-to-stage action)
|
|
5559
|
+
"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. NOTE: internal_update_opportunity (GHL's 'Create/Update Opportunity' / move-to-stage action) IS creatable from scratch \u2014 this tool normalizes it to GHL's exact node shape (workflowsActionType:'INTERNAL' hoisted to the NODE level; a nested copy is what makes GHL reject the node as 'action has a corrupted type'). A synthesized node needs both a pipelineId and a pipelineStageId entry in attributes.__customInputFields__ (use get_pipelines for the IDs); a node round-tripped via get_workflow_full keeps its id and passes through unchanged. 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.",
|
|
5516
5560
|
{
|
|
5517
5561
|
workflowId: import_zod33.z.string().describe("The workflow ID to update."),
|
|
5518
5562
|
name: import_zod33.z.string().optional().describe("New workflow name."),
|
|
@@ -8667,10 +8711,6 @@ ${errors.join("\n")}` : "\nNo errors!",
|
|
|
8667
8711
|
// src/tools/validators.ts
|
|
8668
8712
|
var import_zod47 = require("zod");
|
|
8669
8713
|
var ALL_CATEGORIES = ["pipeline", "stage", "custom_field", "user", "workflow", "form", "calendar", "survey"];
|
|
8670
|
-
var ID_SHAPE = /^([A-Za-z0-9]{17,}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
|
|
8671
|
-
function isIdShaped(s) {
|
|
8672
|
-
return typeof s === "string" && ID_SHAPE.test(s);
|
|
8673
|
-
}
|
|
8674
8714
|
var STANDARD_CONTACT_FIELDS = /* @__PURE__ */ new Set([
|
|
8675
8715
|
"first_name",
|
|
8676
8716
|
"firstname",
|
|
@@ -10921,6 +10961,18 @@ function validateBuildPlan(input) {
|
|
|
10921
10961
|
seen.add(key);
|
|
10922
10962
|
}
|
|
10923
10963
|
}
|
|
10964
|
+
for (const fn of plan.funnels ?? []) {
|
|
10965
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10966
|
+
for (const pg of fn.pages) {
|
|
10967
|
+
const key = pg.name.trim().toLowerCase();
|
|
10968
|
+
if (seen.has(key)) {
|
|
10969
|
+
allErrors.push(
|
|
10970
|
+
`funnel "${fn.ref}" has duplicate page name "${pg.name}" \u2014 page names must be unique within a funnel so each page ref resolves to a single step`
|
|
10971
|
+
);
|
|
10972
|
+
}
|
|
10973
|
+
seen.add(key);
|
|
10974
|
+
}
|
|
10975
|
+
}
|
|
10924
10976
|
const nameGroups = [
|
|
10925
10977
|
["pipelines", (plan.pipelines ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
|
|
10926
10978
|
["customFields", (plan.customFields ?? []).map((x) => ({ ref: x.ref, name: x.name }))],
|
|
@@ -11437,6 +11489,10 @@ function renderReport(plan, result, ctx) {
|
|
|
11437
11489
|
any = true;
|
|
11438
11490
|
L.push(` \u2022 [calendar] ${c.reason}`);
|
|
11439
11491
|
}
|
|
11492
|
+
for (const fn of plan.funnels ?? []) {
|
|
11493
|
+
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).`);
|
|
11495
|
+
}
|
|
11440
11496
|
for (const w of result.workflows) {
|
|
11441
11497
|
for (const m of w.manual) {
|
|
11442
11498
|
any = true;
|
|
@@ -11459,6 +11515,9 @@ function renderReport(plan, result, ctx) {
|
|
|
11459
11515
|
// src/intake-to-build/execute.ts
|
|
11460
11516
|
var norm2 = (s) => s.trim().toLowerCase();
|
|
11461
11517
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11518
|
+
function slugifyName(s) {
|
|
11519
|
+
return s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("-");
|
|
11520
|
+
}
|
|
11462
11521
|
async function executeBackbone(plan, deps, opts = {}) {
|
|
11463
11522
|
const retries = opts.verifyRetries ?? 4;
|
|
11464
11523
|
const backoff = opts.verifyBackoffMs ?? 500;
|
|
@@ -11667,6 +11726,85 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
11667
11726
|
idMap[form.ref] = formId;
|
|
11668
11727
|
built.push({ ref: form.ref, type: "form", name: form.name, status: "created", realId: formId });
|
|
11669
11728
|
}
|
|
11729
|
+
const formNameByRef = new Map((plan.forms ?? []).map((f) => [f.ref, f.name]));
|
|
11730
|
+
const calNameByRef = new Map((plan.calendars ?? []).map((c) => [c.ref, c.name]));
|
|
11731
|
+
const funnelContentReason = (fn) => {
|
|
11732
|
+
const pageBits = fn.pages.map((pg) => {
|
|
11733
|
+
const hosts = [];
|
|
11734
|
+
if (pg.formRef) hosts.push(`hosts form "${formNameByRef.get(pg.formRef) ?? pg.formRef}"`);
|
|
11735
|
+
if (pg.calendarRef) hosts.push(`links calendar "${calNameByRef.get(pg.calendarRef) ?? pg.calendarRef}"`);
|
|
11736
|
+
return `page "${pg.name}"${hosts.length ? ` (${hosts.join(", ")})` : ""}`;
|
|
11737
|
+
});
|
|
11738
|
+
return `Design + publish the pages of funnel "${fn.name}": ${pageBits.join("; ")}. (Blueprint built the funnel + steps; page content/design is manual.)`;
|
|
11739
|
+
};
|
|
11740
|
+
for (const fn of plan.funnels ?? []) {
|
|
11741
|
+
let funnels;
|
|
11742
|
+
try {
|
|
11743
|
+
funnels = await deps.listFunnels();
|
|
11744
|
+
} catch (e) {
|
|
11745
|
+
return halt(fn.ref, "funnel", `could not read existing funnels: ${msg(e)}`);
|
|
11746
|
+
}
|
|
11747
|
+
const matches = funnels.filter((f) => norm2(f.name) === norm2(fn.name));
|
|
11748
|
+
if (matches.length > 1) {
|
|
11749
|
+
return halt(fn.ref, "funnel", `${matches.length} existing funnels are named "${fn.name}" \u2014 ambiguous, cannot safely bind ${fn.ref}. Resolve the duplicate in GHL, then re-run.`);
|
|
11750
|
+
}
|
|
11751
|
+
if (matches.length === 1) {
|
|
11752
|
+
const ex = matches[0];
|
|
11753
|
+
const pageBindings = [];
|
|
11754
|
+
const problems = [];
|
|
11755
|
+
for (const pg of fn.pages) {
|
|
11756
|
+
const stepMatches = ex.steps.filter((s) => norm2(s.name) === norm2(pg.name));
|
|
11757
|
+
if (stepMatches.length === 1) pageBindings.push({ ref: pg.ref, name: pg.name, id: stepMatches[0].id });
|
|
11758
|
+
else if (stepMatches.length === 0) problems.push(`page "${pg.name}" has no matching step`);
|
|
11759
|
+
else problems.push(`page "${pg.name}" matches ${stepMatches.length} steps`);
|
|
11760
|
+
}
|
|
11761
|
+
if (problems.length) {
|
|
11762
|
+
return halt(fn.ref, "funnel", `a funnel named "${fn.name}" already exists but its steps don't match the plan: ${problems.join("; ")}. Align the steps in GHL or remove the funnel, then re-run. (Blueprint never modifies an existing funnel.)`);
|
|
11763
|
+
}
|
|
11764
|
+
idMap[fn.ref] = ex.id;
|
|
11765
|
+
built.push({ ref: fn.ref, type: "funnel", name: fn.name, status: "existing", realId: ex.id });
|
|
11766
|
+
for (const b of pageBindings) {
|
|
11767
|
+
idMap[b.ref] = b.id;
|
|
11768
|
+
built.push({ ref: b.ref, type: "page", name: b.name, status: "existing", realId: b.id });
|
|
11769
|
+
}
|
|
11770
|
+
manual.push({ ref: fn.ref, type: "funnel-page-content", name: fn.name, reason: funnelContentReason(fn) });
|
|
11771
|
+
continue;
|
|
11772
|
+
}
|
|
11773
|
+
let funnelId;
|
|
11774
|
+
try {
|
|
11775
|
+
funnelId = await deps.createFunnel(fn.name);
|
|
11776
|
+
} catch (e) {
|
|
11777
|
+
return halt(fn.ref, "funnel", `create failed: ${msg(e)}`);
|
|
11778
|
+
}
|
|
11779
|
+
if (!funnelId) return halt(fn.ref, "funnel", "funnel create returned no id");
|
|
11780
|
+
const stepBindings = [];
|
|
11781
|
+
try {
|
|
11782
|
+
for (const pg of fn.pages) {
|
|
11783
|
+
const stepId = await deps.createFunnelStep(funnelId, pg.name, `/${slugifyName(pg.name)}`);
|
|
11784
|
+
if (!stepId) throw new Error(`step "${pg.name}" returned no id`);
|
|
11785
|
+
stepBindings.push({ ref: pg.ref, name: pg.name, id: stepId });
|
|
11786
|
+
}
|
|
11787
|
+
} catch (e) {
|
|
11788
|
+
let rolledBack = false;
|
|
11789
|
+
try {
|
|
11790
|
+
await deps.deleteFunnel(funnelId);
|
|
11791
|
+
rolledBack = true;
|
|
11792
|
+
} catch {
|
|
11793
|
+
}
|
|
11794
|
+
return halt(
|
|
11795
|
+
fn.ref,
|
|
11796
|
+
"funnel",
|
|
11797
|
+
rolledBack ? `step create failed; the partial funnel was rolled back (deleted): ${msg(e)}` : `step create failed AND the rollback delete also failed \u2014 manually delete the orphan funnel (id ${funnelId}) in GHL before re-running: ${msg(e)}`
|
|
11798
|
+
);
|
|
11799
|
+
}
|
|
11800
|
+
idMap[fn.ref] = funnelId;
|
|
11801
|
+
built.push({ ref: fn.ref, type: "funnel", name: fn.name, status: "created", realId: funnelId });
|
|
11802
|
+
for (const sb of stepBindings) {
|
|
11803
|
+
idMap[sb.ref] = sb.id;
|
|
11804
|
+
built.push({ ref: sb.ref, type: "page", name: sb.name, status: "created", realId: sb.id });
|
|
11805
|
+
}
|
|
11806
|
+
manual.push({ ref: fn.ref, type: "funnel-page-content", name: fn.name, reason: funnelContentReason(fn) });
|
|
11807
|
+
}
|
|
11670
11808
|
return { ok: true, idMap, built, manual, deferred: deferredSections(plan) };
|
|
11671
11809
|
}
|
|
11672
11810
|
async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMap, built) {
|
|
@@ -11703,7 +11841,6 @@ async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMa
|
|
|
11703
11841
|
}
|
|
11704
11842
|
function deferredSections(plan) {
|
|
11705
11843
|
const out = [];
|
|
11706
|
-
if (plan.funnels?.length) out.push({ section: "funnels", count: plan.funnels.length });
|
|
11707
11844
|
if (plan.workflows?.length) out.push({ section: "workflows", count: plan.workflows.length });
|
|
11708
11845
|
return out;
|
|
11709
11846
|
}
|
|
@@ -11840,6 +11977,38 @@ function pickPipelines(raw) {
|
|
|
11840
11977
|
}
|
|
11841
11978
|
return out;
|
|
11842
11979
|
}
|
|
11980
|
+
function pickFunnels(raw) {
|
|
11981
|
+
const root = raw && typeof raw === "object" ? raw : {};
|
|
11982
|
+
const list = Array.isArray(root.funnels) ? root.funnels : Array.isArray(raw) ? raw : [];
|
|
11983
|
+
const out = [];
|
|
11984
|
+
for (const item of list) {
|
|
11985
|
+
if (!item || typeof item !== "object") continue;
|
|
11986
|
+
const f = item;
|
|
11987
|
+
const id = typeof f._id === "string" ? f._id : typeof f.id === "string" ? f.id : void 0;
|
|
11988
|
+
const name = typeof f.name === "string" ? f.name : void 0;
|
|
11989
|
+
if (!id || !name) continue;
|
|
11990
|
+
const stepsRaw = Array.isArray(f.steps) ? f.steps : [];
|
|
11991
|
+
const steps = stepsRaw.filter((s) => !!s && typeof s === "object").map((s) => ({
|
|
11992
|
+
id: typeof s._id === "string" ? s._id : typeof s.id === "string" ? s.id : "",
|
|
11993
|
+
name: typeof s.name === "string" ? s.name : ""
|
|
11994
|
+
})).filter((s) => s.id && s.name);
|
|
11995
|
+
out.push({ id, name, steps });
|
|
11996
|
+
}
|
|
11997
|
+
return out;
|
|
11998
|
+
}
|
|
11999
|
+
function extractFunnelId(result) {
|
|
12000
|
+
if (!result || typeof result !== "object") return void 0;
|
|
12001
|
+
const r = result;
|
|
12002
|
+
if (typeof r._id === "string") return r._id;
|
|
12003
|
+
if (typeof r.id === "string") return r.id;
|
|
12004
|
+
const funnel = r.funnel;
|
|
12005
|
+
if (funnel && typeof funnel === "object") {
|
|
12006
|
+
const f = funnel;
|
|
12007
|
+
if (typeof f._id === "string") return f._id;
|
|
12008
|
+
if (typeof f.id === "string") return f.id;
|
|
12009
|
+
}
|
|
12010
|
+
return void 0;
|
|
12011
|
+
}
|
|
11843
12012
|
function makeExecuteDeps(client, builderClient, locationId2) {
|
|
11844
12013
|
const pipelineApi = async (method, path7, body) => {
|
|
11845
12014
|
const headers = await builderClient.buildHeaders();
|
|
@@ -11850,6 +12019,27 @@ function makeExecuteDeps(client, builderClient, locationId2) {
|
|
|
11850
12019
|
if (!response.ok) {
|
|
11851
12020
|
const text2 = await response.text();
|
|
11852
12021
|
throw new Error(`Pipeline API ${response.status}: ${method} ${path7}
|
|
12022
|
+
${text2.slice(0, 300)}`);
|
|
12023
|
+
}
|
|
12024
|
+
const text = await response.text();
|
|
12025
|
+
if (!text) return {};
|
|
12026
|
+
try {
|
|
12027
|
+
return JSON.parse(text);
|
|
12028
|
+
} catch {
|
|
12029
|
+
return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
|
|
12030
|
+
}
|
|
12031
|
+
};
|
|
12032
|
+
const funnelApi = async (method, path7, body) => {
|
|
12033
|
+
const headers = await builderClient.buildHeaders();
|
|
12034
|
+
headers.Origin = "https://app.gohighlevel.com";
|
|
12035
|
+
headers.Referer = "https://app.gohighlevel.com/";
|
|
12036
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path7}`;
|
|
12037
|
+
const options = { method, headers };
|
|
12038
|
+
if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
|
|
12039
|
+
const response = await fetch(url, options);
|
|
12040
|
+
if (!response.ok) {
|
|
12041
|
+
const text2 = await response.text();
|
|
12042
|
+
throw new Error(`Funnel API ${response.status}: ${method} ${path7}
|
|
11853
12043
|
${text2.slice(0, 300)}`);
|
|
11854
12044
|
}
|
|
11855
12045
|
const text = await response.text();
|
|
@@ -11959,6 +12149,49 @@ ${text2.slice(0, 300)}`);
|
|
|
11959
12149
|
}
|
|
11960
12150
|
throw err;
|
|
11961
12151
|
}
|
|
12152
|
+
},
|
|
12153
|
+
// ── Funnels ────────────────────────────────────────────────────────────
|
|
12154
|
+
// Internal funnel API. Origin/Referer are REQUIRED on writes (else 401 "Error
|
|
12155
|
+
// calling IAM service") — mirrors funnel-builder.ts.
|
|
12156
|
+
// Paginate so never-clobber sees ALL funnels (a single page would miss a
|
|
12157
|
+
// same-named funnel beyond it → duplicate). Dedup by id and stop when a page
|
|
12158
|
+
// adds nothing new — robust whether or not the endpoint honors `offset`.
|
|
12159
|
+
listFunnels: async () => {
|
|
12160
|
+
const byId = /* @__PURE__ */ new Map();
|
|
12161
|
+
const pageSize = 100;
|
|
12162
|
+
const maxFunnels = 5e3;
|
|
12163
|
+
for (let offset = 0; offset < maxFunnels; offset += pageSize) {
|
|
12164
|
+
const page = pickFunnels(await funnelApi("GET", `/funnel/list?locationId=${locationId2}&limit=${pageSize}&offset=${offset}&getStats=true`));
|
|
12165
|
+
let added = 0;
|
|
12166
|
+
for (const f of page) if (!byId.has(f.id)) {
|
|
12167
|
+
byId.set(f.id, f);
|
|
12168
|
+
added++;
|
|
12169
|
+
}
|
|
12170
|
+
if (page.length < pageSize || added === 0) break;
|
|
12171
|
+
}
|
|
12172
|
+
return [...byId.values()];
|
|
12173
|
+
},
|
|
12174
|
+
createFunnel: async (name) => {
|
|
12175
|
+
const result = await funnelApi("POST", `/funnel/create?locationId=${locationId2}`, {
|
|
12176
|
+
name,
|
|
12177
|
+
locationId: locationId2,
|
|
12178
|
+
type: "funnel",
|
|
12179
|
+
steps: []
|
|
12180
|
+
});
|
|
12181
|
+
const id = extractFunnelId(result);
|
|
12182
|
+
if (!id) throw new Error(`create_funnel returned no id: ${JSON.stringify(result).slice(0, 200)}`);
|
|
12183
|
+
return id;
|
|
12184
|
+
},
|
|
12185
|
+
createFunnelStep: async (funnelId, name, url) => {
|
|
12186
|
+
const stepId = crypto.randomUUID();
|
|
12187
|
+
await funnelApi("POST", `/funnel/create-step`, {
|
|
12188
|
+
funnelId,
|
|
12189
|
+
step: { id: stepId, name, url, pages: [], control_traffic: 100, split: false, type: "optin_funnel_page" }
|
|
12190
|
+
});
|
|
12191
|
+
return stepId;
|
|
12192
|
+
},
|
|
12193
|
+
deleteFunnel: async (funnelId) => {
|
|
12194
|
+
await funnelApi("POST", `/funnel/delete`, { funnelId, locationId: locationId2, userId: builderClient.getUserId() });
|
|
11962
12195
|
}
|
|
11963
12196
|
};
|
|
11964
12197
|
}
|
|
@@ -12017,7 +12250,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
12017
12250
|
);
|
|
12018
12251
|
server2.tool(
|
|
12019
12252
|
"apply_build_plan",
|
|
12020
|
-
`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/
|
|
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.`,
|
|
12021
12254
|
{
|
|
12022
12255
|
plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The approved \xA75 Build Plan object."),
|
|
12023
12256
|
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)."),
|
|
@@ -12092,7 +12325,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
12092
12325
|
}
|
|
12093
12326
|
const deps = makeExecuteDeps(client, builderClient, activeLocation);
|
|
12094
12327
|
const exec = await executeBackbone(typedPlan, deps);
|
|
12095
|
-
const
|
|
12328
|
+
const execManualLines = exec.manual.map((m) => `[${m.type}] ${m.reason}`);
|
|
12096
12329
|
const manualLines = result.workflows.flatMap((w) => [
|
|
12097
12330
|
...w.manual.map((m) => `[${w.name}] ${m.reason}`),
|
|
12098
12331
|
...w.needsContent.map((c) => `[${w.name}] ${c.reason}`)
|
|
@@ -12110,9 +12343,9 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
12110
12343
|
manual: exec.manual,
|
|
12111
12344
|
idMap: exec.idMap,
|
|
12112
12345
|
deferred: exec.deferred,
|
|
12113
|
-
deferredNote: "execute builds the CRM backbone (pipelines, custom fields, tags, custom values), calendars, and
|
|
12114
|
-
nextManualSteps: [...
|
|
12115
|
-
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}
|
|
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.",
|
|
12347
|
+
nextManualSteps: [...execManualLines, ...manualLines, ...handoffLines],
|
|
12348
|
+
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.`
|
|
12116
12349
|
});
|
|
12117
12350
|
}
|
|
12118
12351
|
const collisions = result.items.filter((i) => i.status === "existing");
|
|
@@ -12150,7 +12383,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
12150
12383
|
calendarsManual: result.calendarsManual,
|
|
12151
12384
|
handoffs: result.handoffs,
|
|
12152
12385
|
report,
|
|
12153
|
-
next: 'Review the report. When it looks right, re-run with mode:"execute" to build the CRM backbone
|
|
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.'
|
|
12154
12387
|
});
|
|
12155
12388
|
} catch (error) {
|
|
12156
12389
|
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.41.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",
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
"workflowsActionType": "INTERNAL",
|
|
166
166
|
"type": "internal_update_opportunity"
|
|
167
167
|
},
|
|
168
|
-
"notes": "
|
|
168
|
+
"notes": "CREATABLE from scratch (re-enabled v3.41.0). The discriminator workflowsActionType:'INTERNAL' MUST sit at the NODE level, never nested in attributes — a nested copy makes GHL reject the node as 'action has a corrupted type' and silently fail the whole save. update_workflow_actions normalizes this for you (hoists workflowsActionType to the node level, scaffolds allowBackward + __customInputs__, gives each __customInputFields__ entry an __customInputs__). The shape below (workflowsActionType at the node level alongside type/name/attributes) is correct for both creating and round-tripping. Use pipeline and stage IDs (not names) — get_pipelines / list_pipelines_full to find them FIRST. CRITICAL: if the pipelineId or pipelineStageId don't exist in the target sub-account, GHL silently fails this action AND can kill subsequent actions. A synthesized node needs BOTH a pipelineId and a pipelineStageId entry; a node round-tripped via get_workflow_full keeps its id and passes through unchanged."
|
|
169
169
|
},
|
|
170
170
|
|
|
171
171
|
"_if_else_branching": {
|