@elitedcs/ghl-mcp 3.34.3 → 3.34.5
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 +39 -0
- package/dist/index.js +175 -7
- package/package.json +1 -1
- package/templates/action-schemas.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.34.5 — Update-Opportunity actions are now creatable via the builder
|
|
4
|
+
|
|
5
|
+
- **`update_workflow_actions` can now create `internal_update_opportunity`
|
|
6
|
+
("Update Opportunity" / move-to-stage) actions from scratch.** v3.34.4 only
|
|
7
|
+
guarded against the cryptic "action has a corrupted type" save failure; this
|
|
8
|
+
release fixes the root cause. The bug: `workflowsActionType: "INTERNAL"` was
|
|
9
|
+
being placed inside `attributes`, but GHL's deserializer reads that
|
|
10
|
+
discriminator at the **node** level — a nested one is rejected as corrupted.
|
|
11
|
+
The builder now normalizes the action to GHL's real node shape
|
|
12
|
+
(`workflowsActionType` hoisted to the node level; `allowBackward` + per-field
|
|
13
|
+
`__customInputs__` scaffolding added). Captured from a real UI-built node and
|
|
14
|
+
**proven against a live builder save**. Just pass `__customInputFields__` with
|
|
15
|
+
a `pipelineId` and a `pipelineStageId` entry (validated — both required, so it
|
|
16
|
+
never saves a silent no-op). Round-tripped actions normalize idempotently. Tool
|
|
17
|
+
+ schema docs updated; the v3.34.4 "build it in the UI" guidance is superseded.
|
|
18
|
+
|
|
19
|
+
## 3.34.4 — Audit catches blank-render merge tags + honest guard on Update-Opportunity
|
|
20
|
+
|
|
21
|
+
- **`audit_workflows` / `validate_workflow` now flag dead `{{contact.X}}` merge tags.**
|
|
22
|
+
A custom field deleted or renamed while still referenced by a `{{contact.x}}`
|
|
23
|
+
merge tag in message copy (sms body, email html/subject, notes, notifications)
|
|
24
|
+
renders **blank** at send time. Unlike a dead structured id — which silently
|
|
25
|
+
skips the action and everything after it — this only produces an empty value, so
|
|
26
|
+
it is reported as a **warning**, never an error, and never flips a workflow to
|
|
27
|
+
"broken". Validated by custom-field key (`fieldKey`), with a generous standard-
|
|
28
|
+
field allowlist and nested-path skipping (`{{contact.attributionSource.x}}`) so
|
|
29
|
+
it never cries wolf on `{{contact.first_name}}` and friends; goes `unverified`
|
|
30
|
+
(not warned) if the custom-field list fails to load. Surfaced in a new
|
|
31
|
+
`merge_field_warnings` section + `warnings_count`. (10 regression tests; verified
|
|
32
|
+
against real GHL `fieldKey` shapes.)
|
|
33
|
+
- **`update_workflow_actions`: honest fail-fast on `internal_update_opportunity`.**
|
|
34
|
+
GHL's builder rejects a *synthesized* move/update-opportunity node with "action
|
|
35
|
+
has a corrupted type" and fails the entire save with a cryptic error. The builder
|
|
36
|
+
now detects a freshly-built one and throws a clear, actionable message up front
|
|
37
|
+
(build that one step in the GHL UI, or round-trip an existing one via
|
|
38
|
+
`get_workflow_full` — those keep their node id and pass through untouched). No
|
|
39
|
+
more one-bad-node-nukes-the-whole-save surprise. Tool + schema docs updated to
|
|
40
|
+
state the limitation plainly.
|
|
41
|
+
|
|
3
42
|
## 3.34.3 — Audit catches dead workflow hand-offs + get_social_posts 422 fix
|
|
4
43
|
|
|
5
44
|
- **`audit_workflows` / `validate_workflow` now scan `add_to_workflow` targets.**
|
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.
|
|
34
|
+
version: "3.34.5",
|
|
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",
|
|
@@ -1170,6 +1170,30 @@ function normalizeRemoveFromWorkflowAction(action) {
|
|
|
1170
1170
|
}
|
|
1171
1171
|
return { ...action, attributes: next };
|
|
1172
1172
|
}
|
|
1173
|
+
function normalizeInternalUpdateOpportunityAction(action) {
|
|
1174
|
+
if (action.type !== "internal_update_opportunity") return action;
|
|
1175
|
+
const attrIn = action.attributes && typeof action.attributes === "object" ? action.attributes : {};
|
|
1176
|
+
const fieldsIn = Array.isArray(attrIn.__customInputFields__) ? attrIn.__customInputFields__ : [];
|
|
1177
|
+
const __customInputFields__ = fieldsIn.map((raw) => {
|
|
1178
|
+
const f = raw && typeof raw === "object" ? raw : {};
|
|
1179
|
+
return {
|
|
1180
|
+
__customInputs__: f.__customInputs__ && typeof f.__customInputs__ === "object" ? f.__customInputs__ : {},
|
|
1181
|
+
dataType: typeof f.dataType === "string" ? f.dataType : "SINGLE_OPTIONS",
|
|
1182
|
+
filterField: f.filterField,
|
|
1183
|
+
value: f.value,
|
|
1184
|
+
valueFieldType: typeof f.valueFieldType === "string" ? f.valueFieldType : "select"
|
|
1185
|
+
};
|
|
1186
|
+
});
|
|
1187
|
+
const attributes = {
|
|
1188
|
+
...attrIn,
|
|
1189
|
+
type: "internal_update_opportunity",
|
|
1190
|
+
allowBackward: typeof attrIn.allowBackward === "boolean" ? attrIn.allowBackward : false,
|
|
1191
|
+
__customInputs__: attrIn.__customInputs__ && typeof attrIn.__customInputs__ === "object" ? attrIn.__customInputs__ : {},
|
|
1192
|
+
__customInputFields__
|
|
1193
|
+
};
|
|
1194
|
+
delete attributes.workflowsActionType;
|
|
1195
|
+
return { ...action, workflowsActionType: "INTERNAL", attributes };
|
|
1196
|
+
}
|
|
1173
1197
|
function hasId(action) {
|
|
1174
1198
|
return typeof action.id === "string" && action.id.length > 0;
|
|
1175
1199
|
}
|
|
@@ -1206,9 +1230,13 @@ function validateActionChain(actions) {
|
|
|
1206
1230
|
case "wait":
|
|
1207
1231
|
if (!attr.startAfter) throw new Error(`Wait action "${action.name}" missing required 'startAfter' in attributes.`);
|
|
1208
1232
|
break;
|
|
1209
|
-
case "internal_update_opportunity":
|
|
1210
|
-
|
|
1233
|
+
case "internal_update_opportunity": {
|
|
1234
|
+
const cif = Array.isArray(attr.__customInputFields__) ? attr.__customInputFields__ : null;
|
|
1235
|
+
if (!cif) throw new Error(`Internal update opportunity action "${action.name}" missing '__customInputFields__' array (needs pipelineId + pipelineStageId entries).`);
|
|
1236
|
+
const hasTarget = (ff) => cif.some((f) => f && f.filterField === ff && typeof f.value === "string" && f.value.length > 0);
|
|
1237
|
+
if (!hasTarget("pipelineId") || !hasTarget("pipelineStageId")) throw new Error(`Internal update opportunity action "${action.name}" needs both a pipelineId and a pipelineStageId entry (each with a value) in '__customInputFields__'. Use get_pipelines to find the IDs.`);
|
|
1211
1238
|
break;
|
|
1239
|
+
}
|
|
1212
1240
|
case "remove_from_workflow":
|
|
1213
1241
|
if (!attr.workflowId || !Array.isArray(attr.workflow_id)) throw new Error(`Remove from workflow action "${action.name}" needs BOTH 'workflowId' string AND 'workflow_id' array.`);
|
|
1214
1242
|
break;
|
|
@@ -1827,7 +1855,7 @@ ${errorBody}`
|
|
|
1827
1855
|
*/
|
|
1828
1856
|
buildActionChain(actions) {
|
|
1829
1857
|
validateActionChain(actions);
|
|
1830
|
-
const linked = actions.map(normalizeRemoveFromWorkflowAction).map((action, i) => {
|
|
1858
|
+
const linked = actions.map(normalizeRemoveFromWorkflowAction).map(normalizeInternalUpdateOpportunityAction).map((action, i) => {
|
|
1831
1859
|
const copy = { ...action };
|
|
1832
1860
|
if (!copy.id) {
|
|
1833
1861
|
copy.id = crypto.randomUUID();
|
|
@@ -5394,7 +5422,7 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
5394
5422
|
);
|
|
5395
5423
|
server2.tool(
|
|
5396
5424
|
"update_workflow_actions",
|
|
5397
|
-
"Update a workflow's actions (steps), triggers, name, or status. IMPORTANT: Call get_workflow_full first to see the current state before updating. Handles version tracking automatically. Uses the internal builder API (requires Firebase auth). Action types: sms, email, add_contact_tag, remove_contact_tag, wait, webhook, internal_update_opportunity, custom_code, update_contact_field, add_notes, internal_notification, task_notification, remove_from_workflow, add_to_workflow, goto, transition, workflow_goal. For if/else, call build_if_else_branch and include its returned nodes; if_else is a node discriminator, not a standalone action. For goal events (exit-on-condition nodes), call build_goal_event to get a correctly-shaped workflow_goal node \u2014 wire it in by setting the prior action's `next` to the goal node's id. Trigger types: all 57 native GHL trigger types have typed validation; any unknown trigger type passes through via a permissive fallback so reads never crash.",
|
|
5425
|
+
"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) is creatable \u2014 pass attributes.__customInputFields__ with a pipelineId entry and a pipelineStageId entry (use get_pipelines for the IDs); the builder normalizes it to GHL's node shape (workflowsActionType is set at the node level for you). 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.",
|
|
5398
5426
|
{
|
|
5399
5427
|
workflowId: import_zod33.z.string().describe("The workflow ID to update."),
|
|
5400
5428
|
name: import_zod33.z.string().optional().describe("New workflow name."),
|
|
@@ -8514,6 +8542,96 @@ var ID_SHAPE = /^[A-Za-z0-9]{17,}$/;
|
|
|
8514
8542
|
function isIdShaped(s) {
|
|
8515
8543
|
return typeof s === "string" && ID_SHAPE.test(s);
|
|
8516
8544
|
}
|
|
8545
|
+
var STANDARD_CONTACT_FIELDS = /* @__PURE__ */ new Set([
|
|
8546
|
+
"first_name",
|
|
8547
|
+
"firstname",
|
|
8548
|
+
"last_name",
|
|
8549
|
+
"lastname",
|
|
8550
|
+
"name",
|
|
8551
|
+
"full_name",
|
|
8552
|
+
"fullname",
|
|
8553
|
+
"nickname",
|
|
8554
|
+
"email",
|
|
8555
|
+
"email_lower_case",
|
|
8556
|
+
"phone",
|
|
8557
|
+
"phone_raw",
|
|
8558
|
+
"phone_number",
|
|
8559
|
+
"company_name",
|
|
8560
|
+
"companyname",
|
|
8561
|
+
"business_name",
|
|
8562
|
+
"organization",
|
|
8563
|
+
"address1",
|
|
8564
|
+
"address",
|
|
8565
|
+
"full_address",
|
|
8566
|
+
"street_address",
|
|
8567
|
+
"city",
|
|
8568
|
+
"state",
|
|
8569
|
+
"province",
|
|
8570
|
+
"country",
|
|
8571
|
+
"postal_code",
|
|
8572
|
+
"postalcode",
|
|
8573
|
+
"zip",
|
|
8574
|
+
"zip_code",
|
|
8575
|
+
"timezone",
|
|
8576
|
+
"time_zone",
|
|
8577
|
+
"date_of_birth",
|
|
8578
|
+
"dateofbirth",
|
|
8579
|
+
"dob",
|
|
8580
|
+
"birthday",
|
|
8581
|
+
"source",
|
|
8582
|
+
"contact_source",
|
|
8583
|
+
"type",
|
|
8584
|
+
"contact_type",
|
|
8585
|
+
"id",
|
|
8586
|
+
"contact_id",
|
|
8587
|
+
"contactid",
|
|
8588
|
+
"tags",
|
|
8589
|
+
"dnd",
|
|
8590
|
+
"dnd_status",
|
|
8591
|
+
"assigned_to",
|
|
8592
|
+
"assignedto",
|
|
8593
|
+
"owner",
|
|
8594
|
+
"contact_owner",
|
|
8595
|
+
"created_by",
|
|
8596
|
+
"updated_by",
|
|
8597
|
+
"date_created",
|
|
8598
|
+
"date_updated",
|
|
8599
|
+
"date_added",
|
|
8600
|
+
"created_at",
|
|
8601
|
+
"updated_at",
|
|
8602
|
+
"last_updated",
|
|
8603
|
+
"last_activity",
|
|
8604
|
+
"last_activity_date",
|
|
8605
|
+
"website",
|
|
8606
|
+
"url",
|
|
8607
|
+
"gender",
|
|
8608
|
+
"rating",
|
|
8609
|
+
"additional_emails",
|
|
8610
|
+
"additional_phones",
|
|
8611
|
+
"attributionsource",
|
|
8612
|
+
"attribution_source"
|
|
8613
|
+
]);
|
|
8614
|
+
var MERGE_TAG_RE = /\{\{\s*contact\.([^}|\s]+)/gi;
|
|
8615
|
+
function collectMergeTagKeys(value, out) {
|
|
8616
|
+
if (typeof value === "string") {
|
|
8617
|
+
MERGE_TAG_RE.lastIndex = 0;
|
|
8618
|
+
let m;
|
|
8619
|
+
while ((m = MERGE_TAG_RE.exec(value)) !== null) {
|
|
8620
|
+
const raw = m[1];
|
|
8621
|
+
if (!raw || raw.includes(".")) continue;
|
|
8622
|
+
const key = raw.toLowerCase();
|
|
8623
|
+
if (STANDARD_CONTACT_FIELDS.has(key)) continue;
|
|
8624
|
+
out.add(key);
|
|
8625
|
+
}
|
|
8626
|
+
return;
|
|
8627
|
+
}
|
|
8628
|
+
if (Array.isArray(value)) {
|
|
8629
|
+
for (const v of value) collectMergeTagKeys(v, out);
|
|
8630
|
+
return;
|
|
8631
|
+
}
|
|
8632
|
+
if (value && typeof value === "object")
|
|
8633
|
+
for (const v of Object.values(value)) collectMergeTagKeys(v, out);
|
|
8634
|
+
}
|
|
8517
8635
|
function extractFromTrigger(trigger, refs) {
|
|
8518
8636
|
const triggerName = String(trigger.name ?? trigger.type ?? "unnamed trigger");
|
|
8519
8637
|
const where = `trigger "${triggerName}"`;
|
|
@@ -8668,6 +8786,28 @@ function extractFromAction(action, refs) {
|
|
|
8668
8786
|
break;
|
|
8669
8787
|
}
|
|
8670
8788
|
}
|
|
8789
|
+
const mergeKeys = /* @__PURE__ */ new Set();
|
|
8790
|
+
collectMergeTagKeys(attr, mergeKeys);
|
|
8791
|
+
for (const key of mergeKeys)
|
|
8792
|
+
refs.push({ kind: "custom_field", id: key, where: `${where} \u2192 merge tag {{contact.${key}}}`, byKey: true });
|
|
8793
|
+
}
|
|
8794
|
+
function collectCustomFieldKeys(envelope) {
|
|
8795
|
+
const keys = /* @__PURE__ */ new Set();
|
|
8796
|
+
let arr = null;
|
|
8797
|
+
if (Array.isArray(envelope)) arr = envelope;
|
|
8798
|
+
else if (envelope && typeof envelope === "object" && Array.isArray(envelope.customFields))
|
|
8799
|
+
arr = envelope.customFields;
|
|
8800
|
+
if (arr) for (const item of arr) {
|
|
8801
|
+
if (!item || typeof item !== "object") continue;
|
|
8802
|
+
const o = item;
|
|
8803
|
+
for (const raw of [o.fieldKey, o.key]) {
|
|
8804
|
+
if (typeof raw !== "string") continue;
|
|
8805
|
+
let k = raw.toLowerCase();
|
|
8806
|
+
if (k.startsWith("contact.")) k = k.slice("contact.".length);
|
|
8807
|
+
if (k) keys.add(k);
|
|
8808
|
+
}
|
|
8809
|
+
}
|
|
8810
|
+
return keys;
|
|
8671
8811
|
}
|
|
8672
8812
|
function collectIds(envelope, listKeys) {
|
|
8673
8813
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -8746,12 +8886,13 @@ async function fetchAndBuildLookups(client, builderClient, locationId2, workflow
|
|
|
8746
8886
|
return ids;
|
|
8747
8887
|
}
|
|
8748
8888
|
const custom_field = setFrom("customFields", "custom_field", ["customFields"]);
|
|
8889
|
+
const custom_field_keys = failed.has("customFields") ? /* @__PURE__ */ new Set() : collectCustomFieldKeys(data.customFields);
|
|
8749
8890
|
const user = setFrom("users", "user", ["users"]);
|
|
8750
8891
|
const form = setFrom("forms", "form", ["forms"]);
|
|
8751
8892
|
const calendar = setFrom("calendars", "calendar", ["calendars"]);
|
|
8752
8893
|
const survey = setFrom("surveys", "survey", ["surveys"]);
|
|
8753
8894
|
status.workflow = workflowExistence.complete ? "loaded" : "incomplete";
|
|
8754
|
-
return { pipelines, stages, custom_field, user, workflow: workflowExistence.ids, form, calendar, survey, status };
|
|
8895
|
+
return { pipelines, stages, custom_field, custom_field_keys, user, workflow: workflowExistence.ids, form, calendar, survey, status };
|
|
8755
8896
|
}
|
|
8756
8897
|
function auditOneWorkflow(workflow, selfId, lookups) {
|
|
8757
8898
|
const refs = [];
|
|
@@ -8775,6 +8916,26 @@ function checkRefs(refs, selfId, lookups) {
|
|
|
8775
8916
|
const findings = [];
|
|
8776
8917
|
for (const ref of refs) {
|
|
8777
8918
|
const st = lookups.status[ref.kind];
|
|
8919
|
+
if (ref.byKey && ref.kind === "custom_field") {
|
|
8920
|
+
if (st !== "loaded") {
|
|
8921
|
+
findings.push({
|
|
8922
|
+
severity: "unverified",
|
|
8923
|
+
category: "custom_field",
|
|
8924
|
+
id: ref.id,
|
|
8925
|
+
where: ref.where,
|
|
8926
|
+
message: `merge tag {{contact.${ref.id}}} could not be verified \u2014 the custom field list ${st === "incomplete" ? "is too large to fully load" : "failed to load or was unreadable"}. Re-run; not reported as broken.`
|
|
8927
|
+
});
|
|
8928
|
+
} else if (!lookups.custom_field_keys.has(ref.id)) {
|
|
8929
|
+
findings.push({
|
|
8930
|
+
severity: "warning",
|
|
8931
|
+
category: "custom_field",
|
|
8932
|
+
id: ref.id,
|
|
8933
|
+
where: ref.where,
|
|
8934
|
+
message: `merge tag {{contact.${ref.id}}} matches no standard field or custom field in this location \u2014 it will render blank. If "${ref.id}" was a custom field it was likely deleted or renamed; if it is a standard field this audit does not recognize, you can ignore this.`
|
|
8935
|
+
});
|
|
8936
|
+
}
|
|
8937
|
+
continue;
|
|
8938
|
+
}
|
|
8778
8939
|
if (st !== "loaded") {
|
|
8779
8940
|
findings.push({
|
|
8780
8941
|
severity: "unverified",
|
|
@@ -8846,7 +9007,7 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8846
9007
|
for (const t of Array.isArray(workflow.triggers) ? workflow.triggers : []) extractFromTrigger(t, refs);
|
|
8847
9008
|
for (const a of Array.isArray(workflow.workflowData?.templates) ? workflow.workflowData.templates : []) extractFromAction(a, refs);
|
|
8848
9009
|
if (refs.length === 0)
|
|
8849
|
-
return jsonResponse({ workflowId, workflowName: workflow.name, status: "ok", references_scanned: 0, issues_count: 0, findings: [] });
|
|
9010
|
+
return jsonResponse({ workflowId, workflowName: workflow.name, status: "ok", references_scanned: 0, issues_count: 0, warnings_count: 0, findings: [] });
|
|
8850
9011
|
const needWorkflows = refs.some((r) => r.kind === "workflow");
|
|
8851
9012
|
const catalog = needWorkflows ? await fullWorkflowCatalog(builderClient) : { ids: /* @__PURE__ */ new Set(), complete: true };
|
|
8852
9013
|
const lookups = await fetchAndBuildLookups(client, builderClient, client.defaultLocationId, { ids: catalog.ids, complete: catalog.complete });
|
|
@@ -8857,6 +9018,7 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8857
9018
|
status: findings.some((f) => f.severity === "error") ? "issues_found" : "ok",
|
|
8858
9019
|
references_scanned: refs.length,
|
|
8859
9020
|
issues_count: findings.filter((f) => f.severity === "error").length,
|
|
9021
|
+
warnings_count: findings.filter((f) => f.severity === "warning" && f.category === "custom_field").length,
|
|
8860
9022
|
findings
|
|
8861
9023
|
};
|
|
8862
9024
|
return jsonResponse(report);
|
|
@@ -8898,6 +9060,8 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8898
9060
|
}
|
|
8899
9061
|
const withErrors = results.filter((r) => r.findings.some((f) => f.severity === "error")).map((r) => ({ workflowId: r.id, workflowName: r.name, status: r.status, errors: r.findings.filter((f) => f.severity === "error") })).sort((a, b) => (a.status === "published" ? -1 : 1) - (b.status === "published" ? -1 : 1));
|
|
8900
9062
|
const errorsTotal = withErrors.reduce((n, w) => n + w.errors.length, 0);
|
|
9063
|
+
const withWarnings = results.filter((r) => r.findings.some((f) => f.severity === "warning" && f.category === "custom_field")).map((r) => ({ workflowId: r.id, workflowName: r.name, status: r.status, warnings: r.findings.filter((f) => f.severity === "warning" && f.category === "custom_field") })).sort((a, b) => (a.status === "published" ? -1 : 1) - (b.status === "published" ? -1 : 1));
|
|
9064
|
+
const warningsTotal = withWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
8901
9065
|
const unverifiedCats = ALL_CATEGORIES.filter((c) => lookups.status[c] !== "loaded");
|
|
8902
9066
|
const unverifiedRefs = results.reduce((n, r) => n + r.findings.filter((f) => f.severity === "unverified").length, 0);
|
|
8903
9067
|
return jsonResponse({
|
|
@@ -8909,17 +9073,21 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8909
9073
|
capped: catalog.rows.length > SCAN_CAP,
|
|
8910
9074
|
workflows_with_errors: withErrors.length,
|
|
8911
9075
|
errors_total: errorsTotal,
|
|
9076
|
+
workflows_with_merge_field_warnings: withWarnings.length,
|
|
9077
|
+
merge_field_warnings_total: warningsTotal,
|
|
8912
9078
|
workflows_unscannable: unscannable.length,
|
|
8913
9079
|
workflows_zero_references: zeroRefCount,
|
|
8914
9080
|
unverified: { categories_unloaded: unverifiedCats, references_unverified: unverifiedRefs },
|
|
8915
9081
|
status: errorsTotal > 0 ? "issues_found" : "ok"
|
|
8916
9082
|
},
|
|
8917
9083
|
workflows_with_issues: withErrors,
|
|
9084
|
+
merge_field_warnings: withWarnings,
|
|
8918
9085
|
unscannable,
|
|
8919
9086
|
notes: [
|
|
8920
9087
|
...catalog.complete ? [] : ["Workflow catalog exceeded the pagination backstop \u2014 some workflow-id references shown as unverified."],
|
|
8921
9088
|
...catalog.rows.length > SCAN_CAP ? [`Only the first ${SCAN_CAP} workflows were scanned (account has ${catalog.rows.length}).`] : [],
|
|
8922
9089
|
...unverifiedCats.length ? [`Could not fully load: ${unverifiedCats.join(", ")} \u2014 references to those are 'unverified', not 'broken'. Re-run.`] : [],
|
|
9090
|
+
...warningsTotal ? [`${warningsTotal} {{contact.X}} merge tag(s) reference a field that no longer exists \u2014 these render BLANK in the message but do NOT stop the workflow (warning, not a break). See merge_field_warnings.`] : [],
|
|
8923
9091
|
"workflow_goal, goto, and unrecognized condition types are not deeply checked in this version."
|
|
8924
9092
|
]
|
|
8925
9093
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.34.
|
|
3
|
+
"version": "3.34.5",
|
|
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 (v3.34.5+): pass __customInputFields__ with a pipelineId entry and a pipelineStageId entry; the builder sets workflowsActionType at the NODE level for you. (workflowsActionType must NOT be nested in attributes — GHL rejects a nested one as 'action has a corrupted type'. The builder hoists it automatically; the example here shows it in attributes only for backward-compat input.) Proven against a live builder save 2026-06-14. Use pipeline and stage IDs (not names). Use get_pipelines or list_pipelines_full to find IDs FIRST. CRITICAL: If the pipelineId or stageId don't exist in the target sub-account, GHL silently fails this action AND can kill subsequent actions in the workflow. Always verify IDs exist before deploying."
|
|
169
169
|
},
|
|
170
170
|
|
|
171
171
|
"_if_else_branching": {
|