@elitedcs/ghl-mcp 3.34.2 → 3.34.4
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 +43 -0
- package/dist/index.js +165 -11
- package/package.json +1 -1
- package/templates/action-schemas.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.34.4 — Audit catches blank-render merge tags + honest guard on Update-Opportunity
|
|
4
|
+
|
|
5
|
+
- **`audit_workflows` / `validate_workflow` now flag dead `{{contact.X}}` merge tags.**
|
|
6
|
+
A custom field deleted or renamed while still referenced by a `{{contact.x}}`
|
|
7
|
+
merge tag in message copy (sms body, email html/subject, notes, notifications)
|
|
8
|
+
renders **blank** at send time. Unlike a dead structured id — which silently
|
|
9
|
+
skips the action and everything after it — this only produces an empty value, so
|
|
10
|
+
it is reported as a **warning**, never an error, and never flips a workflow to
|
|
11
|
+
"broken". Validated by custom-field key (`fieldKey`), with a generous standard-
|
|
12
|
+
field allowlist and nested-path skipping (`{{contact.attributionSource.x}}`) so
|
|
13
|
+
it never cries wolf on `{{contact.first_name}}` and friends; goes `unverified`
|
|
14
|
+
(not warned) if the custom-field list fails to load. Surfaced in a new
|
|
15
|
+
`merge_field_warnings` section + `warnings_count`. (10 regression tests; verified
|
|
16
|
+
against real GHL `fieldKey` shapes.)
|
|
17
|
+
- **`update_workflow_actions`: honest fail-fast on `internal_update_opportunity`.**
|
|
18
|
+
GHL's builder rejects a *synthesized* move/update-opportunity node with "action
|
|
19
|
+
has a corrupted type" and fails the entire save with a cryptic error. The builder
|
|
20
|
+
now detects a freshly-built one and throws a clear, actionable message up front
|
|
21
|
+
(build that one step in the GHL UI, or round-trip an existing one via
|
|
22
|
+
`get_workflow_full` — those keep their node id and pass through untouched). No
|
|
23
|
+
more one-bad-node-nukes-the-whole-save surprise. Tool + schema docs updated to
|
|
24
|
+
state the limitation plainly.
|
|
25
|
+
|
|
26
|
+
## 3.34.3 — Audit catches dead workflow hand-offs + get_social_posts 422 fix
|
|
27
|
+
|
|
28
|
+
- **`audit_workflows` / `validate_workflow` now scan `add_to_workflow` targets.**
|
|
29
|
+
Only `remove_from_workflow` was scanned before, so a step that hands a contact
|
|
30
|
+
off to a workflow that was later deleted (the classic cleanup/snapshot mistake)
|
|
31
|
+
went undetected — exactly the silent-skip the audit exists to catch. GHL drops
|
|
32
|
+
the hand-off and every action after it with no error. Now flagged like any
|
|
33
|
+
other dead reference; deduped so the same id in `workflowId` and
|
|
34
|
+
`workflow_id[]` reports once. Still never false-alarms: valid targets pass, and
|
|
35
|
+
a self-reference is a warning, not an error. (4 regression tests added.)
|
|
36
|
+
- **`get_social_posts`:** fixed the GHL 422 ("skip/limit must be a number
|
|
37
|
+
string"; "status should not exist"). `skip`/`limit` are now sent as strings and
|
|
38
|
+
`status` is no longer placed in the request body.
|
|
39
|
+
|
|
40
|
+
Known gap (queued, deferred to avoid false alarms): a custom field deleted or
|
|
41
|
+
renamed but still referenced only through a `{{contact.x}}` merge tag in message
|
|
42
|
+
copy is not yet flagged. That renders blank rather than killing the action, and
|
|
43
|
+
catching it safely needs a key-aware, standard-field-allowlisted pass so the
|
|
44
|
+
audit keeps its never-false-alarm guarantee.
|
|
45
|
+
|
|
3
46
|
## 3.34.2 — Docs: launch pricing + the task-notification trap
|
|
4
47
|
|
|
5
48
|
- README pricing updated for launch: $97/mo at ghlcommand.com, unlimited
|
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.4",
|
|
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",
|
|
@@ -1206,9 +1206,15 @@ function validateActionChain(actions) {
|
|
|
1206
1206
|
case "wait":
|
|
1207
1207
|
if (!attr.startAfter) throw new Error(`Wait action "${action.name}" missing required 'startAfter' in attributes.`);
|
|
1208
1208
|
break;
|
|
1209
|
-
case "internal_update_opportunity":
|
|
1209
|
+
case "internal_update_opportunity": {
|
|
1210
|
+
if (!hasId(action)) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
`Action "${action.name}" (internal_update_opportunity / "Update Opportunity" / move-to-stage) cannot be created via the API yet: GHL's workflow builder rejects a synthesized node with "action has a corrupted type", which silently fails the whole save. Build this one step in the GHL UI (Workflow > add action > Update Opportunity), or pass through an existing one read via get_workflow_full (it keeps its id). Every other action type in this call works.`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1210
1215
|
if (!Array.isArray(attr.__customInputFields__)) throw new Error(`Internal update opportunity action "${action.name}" missing '__customInputFields__' array.`);
|
|
1211
1216
|
break;
|
|
1217
|
+
}
|
|
1212
1218
|
case "remove_from_workflow":
|
|
1213
1219
|
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
1220
|
break;
|
|
@@ -3979,9 +3985,9 @@ function registerSocialPlannerTools(server2, client) {
|
|
|
3979
3985
|
if (fromDate !== void 0) body.fromDate = fromDate;
|
|
3980
3986
|
if (toDate !== void 0) body.toDate = toDate;
|
|
3981
3987
|
if (includeUsers !== void 0) body.includeUsers = includeUsers;
|
|
3982
|
-
|
|
3983
|
-
body.skip = skip ?? 0;
|
|
3984
|
-
body.limit = limit ?? 20;
|
|
3988
|
+
void status;
|
|
3989
|
+
body.skip = String(skip ?? 0);
|
|
3990
|
+
body.limit = String(limit ?? 20);
|
|
3985
3991
|
return client.post(`/social-media-posting/${resolvedLocationId}/posts/list`, { body });
|
|
3986
3992
|
}
|
|
3987
3993
|
);
|
|
@@ -5394,7 +5400,7 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
5394
5400
|
);
|
|
5395
5401
|
server2.tool(
|
|
5396
5402
|
"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.",
|
|
5403
|
+
"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) cannot be created fresh through this API \u2014 GHL's builder rejects a synthesized node with 'action has a corrupted type' and fails the whole save; add that single step in the GHL UI, or pass through one already read via get_workflow_full (it keeps its node id). 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
5404
|
{
|
|
5399
5405
|
workflowId: import_zod33.z.string().describe("The workflow ID to update."),
|
|
5400
5406
|
name: import_zod33.z.string().optional().describe("New workflow name."),
|
|
@@ -8514,6 +8520,96 @@ var ID_SHAPE = /^[A-Za-z0-9]{17,}$/;
|
|
|
8514
8520
|
function isIdShaped(s) {
|
|
8515
8521
|
return typeof s === "string" && ID_SHAPE.test(s);
|
|
8516
8522
|
}
|
|
8523
|
+
var STANDARD_CONTACT_FIELDS = /* @__PURE__ */ new Set([
|
|
8524
|
+
"first_name",
|
|
8525
|
+
"firstname",
|
|
8526
|
+
"last_name",
|
|
8527
|
+
"lastname",
|
|
8528
|
+
"name",
|
|
8529
|
+
"full_name",
|
|
8530
|
+
"fullname",
|
|
8531
|
+
"nickname",
|
|
8532
|
+
"email",
|
|
8533
|
+
"email_lower_case",
|
|
8534
|
+
"phone",
|
|
8535
|
+
"phone_raw",
|
|
8536
|
+
"phone_number",
|
|
8537
|
+
"company_name",
|
|
8538
|
+
"companyname",
|
|
8539
|
+
"business_name",
|
|
8540
|
+
"organization",
|
|
8541
|
+
"address1",
|
|
8542
|
+
"address",
|
|
8543
|
+
"full_address",
|
|
8544
|
+
"street_address",
|
|
8545
|
+
"city",
|
|
8546
|
+
"state",
|
|
8547
|
+
"province",
|
|
8548
|
+
"country",
|
|
8549
|
+
"postal_code",
|
|
8550
|
+
"postalcode",
|
|
8551
|
+
"zip",
|
|
8552
|
+
"zip_code",
|
|
8553
|
+
"timezone",
|
|
8554
|
+
"time_zone",
|
|
8555
|
+
"date_of_birth",
|
|
8556
|
+
"dateofbirth",
|
|
8557
|
+
"dob",
|
|
8558
|
+
"birthday",
|
|
8559
|
+
"source",
|
|
8560
|
+
"contact_source",
|
|
8561
|
+
"type",
|
|
8562
|
+
"contact_type",
|
|
8563
|
+
"id",
|
|
8564
|
+
"contact_id",
|
|
8565
|
+
"contactid",
|
|
8566
|
+
"tags",
|
|
8567
|
+
"dnd",
|
|
8568
|
+
"dnd_status",
|
|
8569
|
+
"assigned_to",
|
|
8570
|
+
"assignedto",
|
|
8571
|
+
"owner",
|
|
8572
|
+
"contact_owner",
|
|
8573
|
+
"created_by",
|
|
8574
|
+
"updated_by",
|
|
8575
|
+
"date_created",
|
|
8576
|
+
"date_updated",
|
|
8577
|
+
"date_added",
|
|
8578
|
+
"created_at",
|
|
8579
|
+
"updated_at",
|
|
8580
|
+
"last_updated",
|
|
8581
|
+
"last_activity",
|
|
8582
|
+
"last_activity_date",
|
|
8583
|
+
"website",
|
|
8584
|
+
"url",
|
|
8585
|
+
"gender",
|
|
8586
|
+
"rating",
|
|
8587
|
+
"additional_emails",
|
|
8588
|
+
"additional_phones",
|
|
8589
|
+
"attributionsource",
|
|
8590
|
+
"attribution_source"
|
|
8591
|
+
]);
|
|
8592
|
+
var MERGE_TAG_RE = /\{\{\s*contact\.([^}|\s]+)/gi;
|
|
8593
|
+
function collectMergeTagKeys(value, out) {
|
|
8594
|
+
if (typeof value === "string") {
|
|
8595
|
+
MERGE_TAG_RE.lastIndex = 0;
|
|
8596
|
+
let m;
|
|
8597
|
+
while ((m = MERGE_TAG_RE.exec(value)) !== null) {
|
|
8598
|
+
const raw = m[1];
|
|
8599
|
+
if (!raw || raw.includes(".")) continue;
|
|
8600
|
+
const key = raw.toLowerCase();
|
|
8601
|
+
if (STANDARD_CONTACT_FIELDS.has(key)) continue;
|
|
8602
|
+
out.add(key);
|
|
8603
|
+
}
|
|
8604
|
+
return;
|
|
8605
|
+
}
|
|
8606
|
+
if (Array.isArray(value)) {
|
|
8607
|
+
for (const v of value) collectMergeTagKeys(v, out);
|
|
8608
|
+
return;
|
|
8609
|
+
}
|
|
8610
|
+
if (value && typeof value === "object")
|
|
8611
|
+
for (const v of Object.values(value)) collectMergeTagKeys(v, out);
|
|
8612
|
+
}
|
|
8517
8613
|
function extractFromTrigger(trigger, refs) {
|
|
8518
8614
|
const triggerName = String(trigger.name ?? trigger.type ?? "unnamed trigger");
|
|
8519
8615
|
const where = `trigger "${triggerName}"`;
|
|
@@ -8610,11 +8706,19 @@ function extractFromAction(action, refs) {
|
|
|
8610
8706
|
refs.push({ kind: "user", id: attr.assignedTo, where: `${where} \u2192 assignedTo` });
|
|
8611
8707
|
break;
|
|
8612
8708
|
}
|
|
8613
|
-
// ── Workflow refs ──
|
|
8709
|
+
// ── Workflow refs — add_to_workflow AND remove_from_workflow both carry a target ──
|
|
8710
|
+
// Both shapes hold the target workflow as a `workflowId` string and/or a `workflow_id`
|
|
8711
|
+
// array. A dead target (e.g. the linked workflow was deleted in a cleanup or snapshot)
|
|
8712
|
+
// is the classic silent-skip: GHL drops the hand-off and every action after it, no error.
|
|
8713
|
+
// Previously only remove_from_workflow was scanned, so add_to_workflow → deleted workflow
|
|
8714
|
+
// went undetected. Dedup so the same id in both fields is reported once.
|
|
8715
|
+
case "add_to_workflow":
|
|
8614
8716
|
case "remove_from_workflow": {
|
|
8615
|
-
|
|
8717
|
+
const targetIds = /* @__PURE__ */ new Set();
|
|
8718
|
+
if (typeof attr.workflowId === "string") targetIds.add(attr.workflowId);
|
|
8616
8719
|
const arr = Array.isArray(attr.workflow_id) ? attr.workflow_id : [];
|
|
8617
|
-
for (
|
|
8720
|
+
for (const v of arr) if (typeof v === "string") targetIds.add(v);
|
|
8721
|
+
for (const id of targetIds) refs.push({ kind: "workflow", id, where: `${where} \u2192 workflow target` });
|
|
8618
8722
|
break;
|
|
8619
8723
|
}
|
|
8620
8724
|
// ── Opportunity refs — FOUR distinct shapes ──
|
|
@@ -8660,6 +8764,28 @@ function extractFromAction(action, refs) {
|
|
|
8660
8764
|
break;
|
|
8661
8765
|
}
|
|
8662
8766
|
}
|
|
8767
|
+
const mergeKeys = /* @__PURE__ */ new Set();
|
|
8768
|
+
collectMergeTagKeys(attr, mergeKeys);
|
|
8769
|
+
for (const key of mergeKeys)
|
|
8770
|
+
refs.push({ kind: "custom_field", id: key, where: `${where} \u2192 merge tag {{contact.${key}}}`, byKey: true });
|
|
8771
|
+
}
|
|
8772
|
+
function collectCustomFieldKeys(envelope) {
|
|
8773
|
+
const keys = /* @__PURE__ */ new Set();
|
|
8774
|
+
let arr = null;
|
|
8775
|
+
if (Array.isArray(envelope)) arr = envelope;
|
|
8776
|
+
else if (envelope && typeof envelope === "object" && Array.isArray(envelope.customFields))
|
|
8777
|
+
arr = envelope.customFields;
|
|
8778
|
+
if (arr) for (const item of arr) {
|
|
8779
|
+
if (!item || typeof item !== "object") continue;
|
|
8780
|
+
const o = item;
|
|
8781
|
+
for (const raw of [o.fieldKey, o.key]) {
|
|
8782
|
+
if (typeof raw !== "string") continue;
|
|
8783
|
+
let k = raw.toLowerCase();
|
|
8784
|
+
if (k.startsWith("contact.")) k = k.slice("contact.".length);
|
|
8785
|
+
if (k) keys.add(k);
|
|
8786
|
+
}
|
|
8787
|
+
}
|
|
8788
|
+
return keys;
|
|
8663
8789
|
}
|
|
8664
8790
|
function collectIds(envelope, listKeys) {
|
|
8665
8791
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -8738,12 +8864,13 @@ async function fetchAndBuildLookups(client, builderClient, locationId2, workflow
|
|
|
8738
8864
|
return ids;
|
|
8739
8865
|
}
|
|
8740
8866
|
const custom_field = setFrom("customFields", "custom_field", ["customFields"]);
|
|
8867
|
+
const custom_field_keys = failed.has("customFields") ? /* @__PURE__ */ new Set() : collectCustomFieldKeys(data.customFields);
|
|
8741
8868
|
const user = setFrom("users", "user", ["users"]);
|
|
8742
8869
|
const form = setFrom("forms", "form", ["forms"]);
|
|
8743
8870
|
const calendar = setFrom("calendars", "calendar", ["calendars"]);
|
|
8744
8871
|
const survey = setFrom("surveys", "survey", ["surveys"]);
|
|
8745
8872
|
status.workflow = workflowExistence.complete ? "loaded" : "incomplete";
|
|
8746
|
-
return { pipelines, stages, custom_field, user, workflow: workflowExistence.ids, form, calendar, survey, status };
|
|
8873
|
+
return { pipelines, stages, custom_field, custom_field_keys, user, workflow: workflowExistence.ids, form, calendar, survey, status };
|
|
8747
8874
|
}
|
|
8748
8875
|
function auditOneWorkflow(workflow, selfId, lookups) {
|
|
8749
8876
|
const refs = [];
|
|
@@ -8767,6 +8894,26 @@ function checkRefs(refs, selfId, lookups) {
|
|
|
8767
8894
|
const findings = [];
|
|
8768
8895
|
for (const ref of refs) {
|
|
8769
8896
|
const st = lookups.status[ref.kind];
|
|
8897
|
+
if (ref.byKey && ref.kind === "custom_field") {
|
|
8898
|
+
if (st !== "loaded") {
|
|
8899
|
+
findings.push({
|
|
8900
|
+
severity: "unverified",
|
|
8901
|
+
category: "custom_field",
|
|
8902
|
+
id: ref.id,
|
|
8903
|
+
where: ref.where,
|
|
8904
|
+
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.`
|
|
8905
|
+
});
|
|
8906
|
+
} else if (!lookups.custom_field_keys.has(ref.id)) {
|
|
8907
|
+
findings.push({
|
|
8908
|
+
severity: "warning",
|
|
8909
|
+
category: "custom_field",
|
|
8910
|
+
id: ref.id,
|
|
8911
|
+
where: ref.where,
|
|
8912
|
+
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.`
|
|
8913
|
+
});
|
|
8914
|
+
}
|
|
8915
|
+
continue;
|
|
8916
|
+
}
|
|
8770
8917
|
if (st !== "loaded") {
|
|
8771
8918
|
findings.push({
|
|
8772
8919
|
severity: "unverified",
|
|
@@ -8838,7 +8985,7 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8838
8985
|
for (const t of Array.isArray(workflow.triggers) ? workflow.triggers : []) extractFromTrigger(t, refs);
|
|
8839
8986
|
for (const a of Array.isArray(workflow.workflowData?.templates) ? workflow.workflowData.templates : []) extractFromAction(a, refs);
|
|
8840
8987
|
if (refs.length === 0)
|
|
8841
|
-
return jsonResponse({ workflowId, workflowName: workflow.name, status: "ok", references_scanned: 0, issues_count: 0, findings: [] });
|
|
8988
|
+
return jsonResponse({ workflowId, workflowName: workflow.name, status: "ok", references_scanned: 0, issues_count: 0, warnings_count: 0, findings: [] });
|
|
8842
8989
|
const needWorkflows = refs.some((r) => r.kind === "workflow");
|
|
8843
8990
|
const catalog = needWorkflows ? await fullWorkflowCatalog(builderClient) : { ids: /* @__PURE__ */ new Set(), complete: true };
|
|
8844
8991
|
const lookups = await fetchAndBuildLookups(client, builderClient, client.defaultLocationId, { ids: catalog.ids, complete: catalog.complete });
|
|
@@ -8849,6 +8996,7 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8849
8996
|
status: findings.some((f) => f.severity === "error") ? "issues_found" : "ok",
|
|
8850
8997
|
references_scanned: refs.length,
|
|
8851
8998
|
issues_count: findings.filter((f) => f.severity === "error").length,
|
|
8999
|
+
warnings_count: findings.filter((f) => f.severity === "warning" && f.category === "custom_field").length,
|
|
8852
9000
|
findings
|
|
8853
9001
|
};
|
|
8854
9002
|
return jsonResponse(report);
|
|
@@ -8890,6 +9038,8 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8890
9038
|
}
|
|
8891
9039
|
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));
|
|
8892
9040
|
const errorsTotal = withErrors.reduce((n, w) => n + w.errors.length, 0);
|
|
9041
|
+
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));
|
|
9042
|
+
const warningsTotal = withWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
8893
9043
|
const unverifiedCats = ALL_CATEGORIES.filter((c) => lookups.status[c] !== "loaded");
|
|
8894
9044
|
const unverifiedRefs = results.reduce((n, r) => n + r.findings.filter((f) => f.severity === "unverified").length, 0);
|
|
8895
9045
|
return jsonResponse({
|
|
@@ -8901,17 +9051,21 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8901
9051
|
capped: catalog.rows.length > SCAN_CAP,
|
|
8902
9052
|
workflows_with_errors: withErrors.length,
|
|
8903
9053
|
errors_total: errorsTotal,
|
|
9054
|
+
workflows_with_merge_field_warnings: withWarnings.length,
|
|
9055
|
+
merge_field_warnings_total: warningsTotal,
|
|
8904
9056
|
workflows_unscannable: unscannable.length,
|
|
8905
9057
|
workflows_zero_references: zeroRefCount,
|
|
8906
9058
|
unverified: { categories_unloaded: unverifiedCats, references_unverified: unverifiedRefs },
|
|
8907
9059
|
status: errorsTotal > 0 ? "issues_found" : "ok"
|
|
8908
9060
|
},
|
|
8909
9061
|
workflows_with_issues: withErrors,
|
|
9062
|
+
merge_field_warnings: withWarnings,
|
|
8910
9063
|
unscannable,
|
|
8911
9064
|
notes: [
|
|
8912
9065
|
...catalog.complete ? [] : ["Workflow catalog exceeded the pagination backstop \u2014 some workflow-id references shown as unverified."],
|
|
8913
9066
|
...catalog.rows.length > SCAN_CAP ? [`Only the first ${SCAN_CAP} workflows were scanned (account has ${catalog.rows.length}).`] : [],
|
|
8914
9067
|
...unverifiedCats.length ? [`Could not fully load: ${unverifiedCats.join(", ")} \u2014 references to those are 'unverified', not 'broken'. Re-run.`] : [],
|
|
9068
|
+
...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.`] : [],
|
|
8915
9069
|
"workflow_goal, goto, and unrecognized condition types are not deeply checked in this version."
|
|
8916
9070
|
]
|
|
8917
9071
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.34.
|
|
3
|
+
"version": "3.34.4",
|
|
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": "CANNOT CREATE FRESH VIA THIS API: GHL's builder rejects a synthesized internal_update_opportunity node with 'action has a corrupted type' and fails the whole save. Build this step in the GHL UI, or round-trip one read via get_workflow_full (it keeps its node id). The shape below is correct for READING / round-tripping. 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": {
|