@elitedcs/ghl-mcp 3.69.0 → 3.70.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/CHANGELOG.md +37 -0
- package/dist/index.js +180 -11
- package/package.json +1 -1
- package/templates/action-schemas.json +23 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.70.0 — a clean validation now means something
|
|
4
|
+
|
|
5
|
+
A subscriber built an Instagram-comment-to-DM workflow, ran `validate_workflow`, got
|
|
6
|
+
back "0 issues, 0 warnings", published it, and watched the Create/Update Opportunity
|
|
7
|
+
step do nothing: no card in the pipeline, no card on the contact, and the workflow
|
|
8
|
+
carried on to the next step as if it had worked. They were right about why. The step
|
|
9
|
+
had only a pipeline and a stage. GoHighLevel's own documentation lists Opportunity
|
|
10
|
+
Name, Source and Status as mandatory for the combined Create/Update Opportunity step
|
|
11
|
+
to create anything; with only pipeline and stage it moves a card the contact already
|
|
12
|
+
has, and a brand-new lead has none. The
|
|
13
|
+
validator was checking that every id existed. It never asked whether the step could
|
|
14
|
+
do what its name says.
|
|
15
|
+
|
|
16
|
+
**`validate_workflow` and `audit_workflows` now check what a step can do.** A
|
|
17
|
+
Create/Update Opportunity step with only pipeline + stage and no opportunity in
|
|
18
|
+
context comes back as a warning that says exactly that and names the fix. "In
|
|
19
|
+
context" is worked out from the workflow itself: a create step earlier on the same
|
|
20
|
+
path, a Find Opportunity step whose "Opportunity Found" branch the step sits in, or a
|
|
21
|
+
trigger that fires on an opportunity. A step that carries an Opportunity Name and a
|
|
22
|
+
Status is taken as one that creates and is left alone; a Name with no Status is
|
|
23
|
+
reported as unverified with the thing to check. Before release the check ran against
|
|
24
|
+
every workflow in five of our own accounts (106 workflows) and flagged none; the run
|
|
25
|
+
is recorded in `docs/proofs/2026-08-26-validate-runtime-noop.md`. A path the validator
|
|
26
|
+
cannot trace, or a field this version does not know, is reported as unverified rather
|
|
27
|
+
than passed. `warnings_count` now includes these findings, the report says how many
|
|
28
|
+
steps were checked (`actions_checked`), and `audit_workflows` lists them under
|
|
29
|
+
`shape_warnings` with counts in its summary; `status` still flips only on an error,
|
|
30
|
+
never on a warning. A `task_notification` step spelled with the underscore, which
|
|
31
|
+
saves, validates and is skipped at runtime, is that kind of error now.
|
|
32
|
+
|
|
33
|
+
**Claude now builds the right node.** GoHighLevel has separated Create Opportunity
|
|
34
|
+
from Update Opportunity and is phasing the combined action out for new workflows. The
|
|
35
|
+
builder knows the newer Create Opportunity node (`internal_create_opportunity`),
|
|
36
|
+
validates it, normalizes it to the shape proven to create a card, and the reference
|
|
37
|
+
material Claude reads before building a workflow now says which node creates a card
|
|
38
|
+
and which one moves it, instead of pointing both jobs at the same node. (#57)
|
|
39
|
+
|
|
3
40
|
## 3.69.0 — the cockpit hardened, and the plan you approved is the plan that runs
|
|
4
41
|
|
|
5
42
|
Two items in this release exist because someone tried to break the cockpit
|
package/dist/index.js
CHANGED
|
@@ -1118,6 +1118,35 @@ function normalizeRemoveFromWorkflowAction(action) {
|
|
|
1118
1118
|
}
|
|
1119
1119
|
return { ...action, attributes: next };
|
|
1120
1120
|
}
|
|
1121
|
+
function normalizeInternalCreateOpportunityAction(action) {
|
|
1122
|
+
if (action.type !== "internal_create_opportunity") return action;
|
|
1123
|
+
const attrIn = action.attributes && typeof action.attributes === "object" ? action.attributes : {};
|
|
1124
|
+
const fieldsIn = Array.isArray(attrIn.__customInputFields__) ? attrIn.__customInputFields__ : [];
|
|
1125
|
+
let pipelineId = typeof attrIn.pipelineId === "string" ? attrIn.pipelineId : void 0;
|
|
1126
|
+
const __customInputFields__ = [];
|
|
1127
|
+
for (const raw of fieldsIn) {
|
|
1128
|
+
const f = raw && typeof raw === "object" ? raw : {};
|
|
1129
|
+
if (f.filterField === "pipelineId") {
|
|
1130
|
+
if (!pipelineId && typeof f.value === "string") pipelineId = f.value;
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
__customInputFields__.push({
|
|
1134
|
+
...f,
|
|
1135
|
+
__customInputs__: f.__customInputs__ && typeof f.__customInputs__ === "object" ? f.__customInputs__ : {},
|
|
1136
|
+
dataType: typeof f.dataType === "string" ? f.dataType : "SINGLE_OPTIONS",
|
|
1137
|
+
valueFieldType: typeof f.valueFieldType === "string" ? f.valueFieldType : "select"
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
const attributes = {
|
|
1141
|
+
...attrIn,
|
|
1142
|
+
type: "internal_create_opportunity",
|
|
1143
|
+
...pipelineId ? { pipelineId } : {},
|
|
1144
|
+
__customInputs__: attrIn.__customInputs__ && typeof attrIn.__customInputs__ === "object" ? attrIn.__customInputs__ : {},
|
|
1145
|
+
__customInputFields__
|
|
1146
|
+
};
|
|
1147
|
+
delete attributes.workflowsActionType;
|
|
1148
|
+
return { ...action, workflowsActionType: "INTERNAL", attributes };
|
|
1149
|
+
}
|
|
1121
1150
|
function normalizeInternalUpdateOpportunityAction(action) {
|
|
1122
1151
|
if (action.type !== "internal_update_opportunity") return action;
|
|
1123
1152
|
const attrIn = action.attributes && typeof action.attributes === "object" ? action.attributes : {};
|
|
@@ -1202,6 +1231,26 @@ function validateActionChain(actions, existingIds) {
|
|
|
1202
1231
|
}
|
|
1203
1232
|
break;
|
|
1204
1233
|
}
|
|
1234
|
+
case "internal_create_opportunity": {
|
|
1235
|
+
const isRoundTripped = hasId(action) && (existingIds ? existingIds.has(action.id) : true);
|
|
1236
|
+
if (!isRoundTripped) {
|
|
1237
|
+
const cif = Array.isArray(attr.__customInputFields__) ? attr.__customInputFields__ : [];
|
|
1238
|
+
const listed = (ff) => cif.find((f) => f && f.filterField === ff);
|
|
1239
|
+
const pipelineId = typeof attr.pipelineId === "string" ? attr.pipelineId : listed("pipelineId")?.value;
|
|
1240
|
+
if (!isIdShaped(pipelineId)) {
|
|
1241
|
+
throw new Error(
|
|
1242
|
+
`Create opportunity action "${action.name}" needs a valid 'pipelineId' (an id, not a name) in attributes. Use get_pipelines / list_pipelines_full to find it.`
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
const stage = listed("pipelineStageId");
|
|
1246
|
+
if (!stage || !isIdShaped(stage.value)) {
|
|
1247
|
+
throw new Error(
|
|
1248
|
+
`Create opportunity action "${action.name}" needs a valid pipelineStageId 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.)`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1205
1254
|
case "remove_from_workflow":
|
|
1206
1255
|
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.`);
|
|
1207
1256
|
break;
|
|
@@ -1941,7 +1990,7 @@ ${errorBody}`
|
|
|
1941
1990
|
*/
|
|
1942
1991
|
buildActionChain(actions, existingIds) {
|
|
1943
1992
|
validateActionChain(actions, existingIds);
|
|
1944
|
-
const linked = actions.map(normalizeRemoveFromWorkflowAction).map(normalizeInternalUpdateOpportunityAction).map((action, i) => {
|
|
1993
|
+
const linked = actions.map(normalizeRemoveFromWorkflowAction).map(normalizeInternalUpdateOpportunityAction).map(normalizeInternalCreateOpportunityAction).map((action, i) => {
|
|
1945
1994
|
const copy = { ...action };
|
|
1946
1995
|
if (!copy.id) {
|
|
1947
1996
|
copy.id = crypto.randomUUID();
|
|
@@ -12881,6 +12930,109 @@ var init_template_deployer = __esm({
|
|
|
12881
12930
|
});
|
|
12882
12931
|
|
|
12883
12932
|
// src/tools/validators.ts
|
|
12933
|
+
function traceOpportunityContext(node, byId2) {
|
|
12934
|
+
let cur = node;
|
|
12935
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12936
|
+
for (let hops = 0; hops < MAX_TRACE_HOPS; hops++) {
|
|
12937
|
+
const pk = cur.parentKey;
|
|
12938
|
+
if (typeof pk !== "string" || !pk) return "none";
|
|
12939
|
+
if (seen.has(pk)) return "dangling";
|
|
12940
|
+
seen.add(pk);
|
|
12941
|
+
const parent = byId2.get(pk);
|
|
12942
|
+
if (!parent) return "dangling";
|
|
12943
|
+
const ptype = typeof parent.type === "string" ? parent.type : "";
|
|
12944
|
+
if (OPPORTUNITY_CREATING_TYPES.has(ptype)) return "create";
|
|
12945
|
+
if (ptype === "transition") {
|
|
12946
|
+
const ownerId = typeof parent.parentKey === "string" ? parent.parentKey : typeof parent.parent === "string" ? parent.parent : "";
|
|
12947
|
+
const owner = ownerId ? byId2.get(ownerId) : void 0;
|
|
12948
|
+
if (owner && owner.type === "find_opportunity") {
|
|
12949
|
+
const meta = parent.attributes?.meta ?? {};
|
|
12950
|
+
const label = `${typeof parent.name === "string" ? parent.name : ""} ${typeof meta.__branchKey__ === "string" ? meta.__branchKey__ : ""}`.toLowerCase();
|
|
12951
|
+
if (label.includes("opportunity not found")) return "not_found";
|
|
12952
|
+
if (label.includes("opportunity found")) return "found";
|
|
12953
|
+
}
|
|
12954
|
+
}
|
|
12955
|
+
cur = parent;
|
|
12956
|
+
}
|
|
12957
|
+
return "dangling";
|
|
12958
|
+
}
|
|
12959
|
+
function checkActionShapes(workflow) {
|
|
12960
|
+
const findings = [];
|
|
12961
|
+
const actions = Array.isArray(workflow.workflowData?.templates) ? workflow.workflowData.templates : [];
|
|
12962
|
+
const triggers = Array.isArray(workflow.triggers) ? workflow.triggers : [];
|
|
12963
|
+
const triggerGivesOpportunity = triggers.some((t) => typeof t.type === "string" && OPPORTUNITY_CONTEXT_TRIGGERS.has(t.type));
|
|
12964
|
+
const byId2 = /* @__PURE__ */ new Map();
|
|
12965
|
+
for (const a of actions) if (typeof a.id === "string") byId2.set(a.id, a);
|
|
12966
|
+
for (const a of actions) {
|
|
12967
|
+
const type = typeof a.type === "string" ? a.type : "unknown";
|
|
12968
|
+
const name = typeof a.name === "string" ? a.name : "unnamed action";
|
|
12969
|
+
const id = typeof a.id === "string" ? a.id : name;
|
|
12970
|
+
const where = `action "${name}" (${type})`;
|
|
12971
|
+
if (type === "task_notification") {
|
|
12972
|
+
findings.push({
|
|
12973
|
+
severity: "error",
|
|
12974
|
+
category: "action_shape",
|
|
12975
|
+
id,
|
|
12976
|
+
where,
|
|
12977
|
+
message: `${where} saves and validates but is SKIPPED at runtime \u2014 the type must be "task-notification" (hyphen). Re-save it with the hyphenated type.`
|
|
12978
|
+
});
|
|
12979
|
+
continue;
|
|
12980
|
+
}
|
|
12981
|
+
if (type !== "internal_update_opportunity") continue;
|
|
12982
|
+
const attr = a.attributes ?? {};
|
|
12983
|
+
const cif = Array.isArray(attr.__customInputFields__) ? attr.__customInputFields__ : [];
|
|
12984
|
+
const keys = cif.map((f) => f && typeof f.filterField === "string" ? f.filterField : "").filter(Boolean);
|
|
12985
|
+
const has = (k) => cif.some((f) => f && f.filterField === k && typeof f.value === "string" && f.value.trim() !== "");
|
|
12986
|
+
if (has("name") && has("status")) continue;
|
|
12987
|
+
if (has("name")) {
|
|
12988
|
+
findings.push({
|
|
12989
|
+
severity: "unverified",
|
|
12990
|
+
category: "action_shape",
|
|
12991
|
+
id,
|
|
12992
|
+
where,
|
|
12993
|
+
message: `${where} carries an opportunity name but no status \u2014 GoHighLevel lists Name, Source and Status as mandatory to create a card. Open the step in the GHL builder and confirm Status (and Source) are set, or use Create Opportunity (internal_create_opportunity).`
|
|
12994
|
+
});
|
|
12995
|
+
continue;
|
|
12996
|
+
}
|
|
12997
|
+
const unknown = keys.filter((k) => !KNOWN_UPDATE_OPP_FIELDS.has(k));
|
|
12998
|
+
if (unknown.length) {
|
|
12999
|
+
findings.push({
|
|
13000
|
+
severity: "unverified",
|
|
13001
|
+
category: "action_shape",
|
|
13002
|
+
id,
|
|
13003
|
+
where,
|
|
13004
|
+
message: `${where} carries field(s) this version does not know (${unknown.join(", ")}) \u2014 could not judge whether it can create an opportunity.`
|
|
13005
|
+
});
|
|
13006
|
+
continue;
|
|
13007
|
+
}
|
|
13008
|
+
const ctx = traceOpportunityContext(a, byId2);
|
|
13009
|
+
if (ctx === "create" || ctx === "found") continue;
|
|
13010
|
+
if (ctx === "dangling") {
|
|
13011
|
+
findings.push({
|
|
13012
|
+
severity: "unverified",
|
|
13013
|
+
category: "action_shape",
|
|
13014
|
+
id,
|
|
13015
|
+
where,
|
|
13016
|
+
message: `${where} could not be traced back to the trigger (a parentKey points to a missing step) \u2014 could not judge whether an opportunity is in context.`
|
|
13017
|
+
});
|
|
13018
|
+
continue;
|
|
13019
|
+
}
|
|
13020
|
+
if (ctx === "none" && triggerGivesOpportunity) continue;
|
|
13021
|
+
findings.push({
|
|
13022
|
+
severity: "warning",
|
|
13023
|
+
category: "action_shape",
|
|
13024
|
+
id,
|
|
13025
|
+
where,
|
|
13026
|
+
message: `${where} cannot create an opportunity: it has only pipeline + stage${has("status") ? " + status" : ""}. It moves a card the contact already has in that pipeline; a contact without one (a new lead) gets no card and the workflow continues as if it worked. To create a card use Create Opportunity (internal_create_opportunity); to move one, put Find Opportunity before this step or trigger on the opportunity. GoHighLevel is phasing the combined Create/Update action out.`
|
|
13027
|
+
});
|
|
13028
|
+
}
|
|
13029
|
+
return findings;
|
|
13030
|
+
}
|
|
13031
|
+
function summarizeFindings(findings) {
|
|
13032
|
+
const issues = findings.filter((f) => f.severity === "error").length;
|
|
13033
|
+
const warnings = findings.filter((f) => f.severity === "warning" && (f.category === "custom_field" || f.category === "action_shape")).length;
|
|
13034
|
+
return { status: issues > 0 ? "issues_found" : "ok", issues_count: issues, warnings_count: warnings };
|
|
13035
|
+
}
|
|
12884
13036
|
function collectMergeTagKeys(value, out) {
|
|
12885
13037
|
if (typeof value === "string") {
|
|
12886
13038
|
MERGE_TAG_RE.lastIndex = 0;
|
|
@@ -13294,28 +13446,29 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
13294
13446
|
}
|
|
13295
13447
|
server2.tool(
|
|
13296
13448
|
"validate_workflow",
|
|
13297
|
-
"Pre-flight
|
|
13449
|
+
"Pre-flight validation for ONE deployed GHL workflow. Also flags steps that cannot do what their name says at runtime (a Create/Update Opportunity step with only pipeline + stage cannot create a card; task_notification is skipped) \u2014 read every finding with severity warning, not just issues_count. Scans every trigger and action for references to pipelines, pipeline stages, custom fields, users, workflows, forms, calendars, and surveys; verifies each ID exists in the current location. Use BEFORE publish_workflow when a workflow was edited, or when a published workflow stops behaving. Catches the silent-failure bug where invalid IDs make GHL skip all subsequent actions. Never reports a false break \u2014 anything it cannot fully verify is marked 'unverified', not 'error'.",
|
|
13298
13450
|
{ workflowId: import_zod53.z.string().describe("The workflow ID to validate.") },
|
|
13299
13451
|
async ({ workflowId }) => {
|
|
13300
13452
|
try {
|
|
13301
13453
|
const workflow = await builderClient.getWorkflow(workflowId);
|
|
13302
13454
|
if (!workflow) return errorResponse(new Error(`Workflow ${workflowId} not found`));
|
|
13303
13455
|
const refs = [];
|
|
13456
|
+
const shape = checkActionShapes(workflow);
|
|
13457
|
+
const actionsChecked = Array.isArray(workflow.workflowData?.templates) ? workflow.workflowData.templates.length : 0;
|
|
13304
13458
|
for (const t of Array.isArray(workflow.triggers) ? workflow.triggers : []) extractFromTrigger(t, refs);
|
|
13305
13459
|
for (const a of Array.isArray(workflow.workflowData?.templates) ? workflow.workflowData.templates : []) extractFromAction(a, refs);
|
|
13306
13460
|
if (refs.length === 0)
|
|
13307
|
-
return jsonResponse({ workflowId, workflowName: workflow.name,
|
|
13461
|
+
return jsonResponse({ workflowId, workflowName: workflow.name, ...summarizeFindings(shape), references_scanned: 0, actions_checked: actionsChecked, findings: shape });
|
|
13308
13462
|
const needWorkflows = refs.some((r) => r.kind === "workflow");
|
|
13309
13463
|
const catalog = needWorkflows ? await fullWorkflowCatalog(builderClient) : { ids: /* @__PURE__ */ new Set(), complete: true };
|
|
13310
13464
|
const lookups = await fetchAndBuildLookups(client, builderClient, client.defaultLocationId, { ids: catalog.ids, complete: catalog.complete });
|
|
13311
|
-
const findings = checkRefs(refs, workflowId, lookups);
|
|
13465
|
+
const findings = [...shape, ...checkRefs(refs, workflowId, lookups)];
|
|
13312
13466
|
const report = {
|
|
13313
13467
|
workflowId,
|
|
13314
13468
|
workflowName: workflow.name,
|
|
13315
|
-
|
|
13469
|
+
...summarizeFindings(findings),
|
|
13316
13470
|
references_scanned: refs.length,
|
|
13317
|
-
|
|
13318
|
-
warnings_count: findings.filter((f) => f.severity === "warning" && f.category === "custom_field").length,
|
|
13471
|
+
actions_checked: actionsChecked,
|
|
13319
13472
|
findings
|
|
13320
13473
|
};
|
|
13321
13474
|
return jsonResponse(report);
|
|
@@ -13326,7 +13479,7 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
13326
13479
|
);
|
|
13327
13480
|
server2.tool(
|
|
13328
13481
|
"audit_workflows",
|
|
13329
|
-
"Account-wide silent-failure audit:
|
|
13482
|
+
"Account-wide silent-failure audit: also flags steps that cannot do what their name says (a Create/Update Opportunity step with only pipeline + stage cannot create a card) under shape_warnings \u2014 read them. Scans EVERY workflow in the current location for references to pipelines/stages/custom-fields/users/workflows/forms/calendars/surveys that don't exist \u2014 the GHL bug where one bad ID silently kills that action and all actions after it. Returns a prioritized report of what's broken, what couldn't be scanned, and what couldn't be fully verified. Conservative: never reports a false break (uncertain checks are 'unverified', not 'broken'). Read-only.",
|
|
13330
13483
|
{},
|
|
13331
13484
|
async () => {
|
|
13332
13485
|
try {
|
|
@@ -13348,7 +13501,7 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
13348
13501
|
const wf = await builderClient.getWorkflow(row.id);
|
|
13349
13502
|
const refs = auditOneWorkflow(wf, row.id, lookups);
|
|
13350
13503
|
if (refs.length === 0) zeroRefCount++;
|
|
13351
|
-
const findings = checkRefs(refs, row.id, lookups);
|
|
13504
|
+
const findings = [...checkActionShapes(wf), ...checkRefs(refs, row.id, lookups)];
|
|
13352
13505
|
results.push({ id: row.id, name: wf.name ?? row.name, status: wf.status, refs: refs.length, findings });
|
|
13353
13506
|
} catch (e) {
|
|
13354
13507
|
unscannable.push({ id: row.id, name: row.name, reason: e instanceof Error ? e.message : String(e) });
|
|
@@ -13359,6 +13512,8 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
13359
13512
|
const errorsTotal = withErrors.reduce((n, w) => n + w.errors.length, 0);
|
|
13360
13513
|
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));
|
|
13361
13514
|
const warningsTotal = withWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
13515
|
+
const withShapeWarnings = results.filter((r) => r.findings.some((f) => f.severity === "warning" && f.category === "action_shape")).map((r) => ({ workflowId: r.id, workflowName: r.name, status: r.status, warnings: r.findings.filter((f) => f.severity === "warning" && f.category === "action_shape") })).sort((a, b) => (a.status === "published" ? -1 : 1) - (b.status === "published" ? -1 : 1));
|
|
13516
|
+
const shapeWarningsTotal = withShapeWarnings.reduce((n, w) => n + w.warnings.length, 0);
|
|
13362
13517
|
const unverifiedCats = ALL_CATEGORIES.filter((c) => lookups.status[c] !== "loaded");
|
|
13363
13518
|
const unverifiedRefs = results.reduce((n, r) => n + r.findings.filter((f) => f.severity === "unverified").length, 0);
|
|
13364
13519
|
return jsonResponse({
|
|
@@ -13372,6 +13527,8 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
13372
13527
|
errors_total: errorsTotal,
|
|
13373
13528
|
workflows_with_merge_field_warnings: withWarnings.length,
|
|
13374
13529
|
merge_field_warnings_total: warningsTotal,
|
|
13530
|
+
workflows_with_shape_warnings: withShapeWarnings.length,
|
|
13531
|
+
shape_warnings_total: shapeWarningsTotal,
|
|
13375
13532
|
workflows_unscannable: unscannable.length,
|
|
13376
13533
|
workflows_zero_references: zeroRefCount,
|
|
13377
13534
|
unverified: { categories_unloaded: unverifiedCats, references_unverified: unverifiedRefs },
|
|
@@ -13379,12 +13536,14 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
13379
13536
|
},
|
|
13380
13537
|
workflows_with_issues: withErrors,
|
|
13381
13538
|
merge_field_warnings: withWarnings,
|
|
13539
|
+
shape_warnings: withShapeWarnings,
|
|
13382
13540
|
unscannable,
|
|
13383
13541
|
notes: [
|
|
13384
13542
|
...catalog.complete ? [] : ["Workflow catalog exceeded the pagination backstop \u2014 some workflow-id references shown as unverified."],
|
|
13385
13543
|
...catalog.rows.length > SCAN_CAP ? [`Only the first ${SCAN_CAP} workflows were scanned (account has ${catalog.rows.length}).`] : [],
|
|
13386
13544
|
...unverifiedCats.length ? [`Could not fully load: ${unverifiedCats.join(", ")} \u2014 references to those are 'unverified', not 'broken'. Re-run.`] : [],
|
|
13387
13545
|
...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.`] : [],
|
|
13546
|
+
...shapeWarningsTotal ? [`${shapeWarningsTotal} Create/Update Opportunity step(s) have only pipeline + stage \u2014 they cannot create a card for a new contact (see shape_warnings).`] : [],
|
|
13388
13547
|
"workflow_goal, goto, and unrecognized condition types are not deeply checked in this version."
|
|
13389
13548
|
]
|
|
13390
13549
|
});
|
|
@@ -13394,7 +13553,7 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
13394
13553
|
}
|
|
13395
13554
|
);
|
|
13396
13555
|
}
|
|
13397
|
-
var import_zod53, ALL_CATEGORIES, STANDARD_CONTACT_FIELDS, MERGE_TAG_RE;
|
|
13556
|
+
var import_zod53, ALL_CATEGORIES, OPPORTUNITY_CONTEXT_TRIGGERS, OPPORTUNITY_CREATING_TYPES, KNOWN_UPDATE_OPP_FIELDS, MAX_TRACE_HOPS, STANDARD_CONTACT_FIELDS, MERGE_TAG_RE;
|
|
13398
13557
|
var init_validators = __esm({
|
|
13399
13558
|
"src/tools/validators.ts"() {
|
|
13400
13559
|
"use strict";
|
|
@@ -13403,6 +13562,16 @@ var init_validators = __esm({
|
|
|
13403
13562
|
init_api_schemas();
|
|
13404
13563
|
init_id_shape();
|
|
13405
13564
|
ALL_CATEGORIES = ["pipeline", "stage", "custom_field", "user", "workflow", "form", "calendar", "survey"];
|
|
13565
|
+
OPPORTUNITY_CONTEXT_TRIGGERS = /* @__PURE__ */ new Set([
|
|
13566
|
+
"opportunity_status_changed",
|
|
13567
|
+
"opportunity_created",
|
|
13568
|
+
"opportunity_changed",
|
|
13569
|
+
"pipeline_stage_updated",
|
|
13570
|
+
"opportunity_decay"
|
|
13571
|
+
]);
|
|
13572
|
+
OPPORTUNITY_CREATING_TYPES = /* @__PURE__ */ new Set(["internal_create_opportunity", "create_opportunity"]);
|
|
13573
|
+
KNOWN_UPDATE_OPP_FIELDS = /* @__PURE__ */ new Set(["pipelineId", "pipelineStageId", "name", "status", "source", "monetaryValue"]);
|
|
13574
|
+
MAX_TRACE_HOPS = 200;
|
|
13406
13575
|
STANDARD_CONTACT_FIELDS = /* @__PURE__ */ new Set([
|
|
13407
13576
|
"first_name",
|
|
13408
13577
|
"firstname",
|
|
@@ -18799,7 +18968,7 @@ var require_package = __commonJS({
|
|
|
18799
18968
|
"package.json"(exports2, module2) {
|
|
18800
18969
|
module2.exports = {
|
|
18801
18970
|
name: "@elitedcs/ghl-mcp",
|
|
18802
|
-
version: "3.
|
|
18971
|
+
version: "3.70.0",
|
|
18803
18972
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
18804
18973
|
description: "GoHighLevel MCP Server for Claude. 242 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
18805
18974
|
main: "dist/index.js",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.70.0",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
5
|
"description": "GoHighLevel MCP Server for Claude. 242 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"selectedUser": "USER_ID"
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
|
-
"notes": "Nested 'notification' object REQUIRED. selectedUser MUST be a real user ID
|
|
109
|
+
"notes": "Nested 'notification' object REQUIRED. selectedUser MUST be a real user ID \u2014 GHL now REJECTS an empty string (live-verified 2026-08-06; the old 'empty = all users' behavior is gone). Use get_users to find IDs. EMAIL CHANNEL (verified live 2026-07-20, PWDJ workflow e91f28da): attributes.type is 'email' (NOT 'notification'), nested key is 'email' (NOT 'notification'), body field is 'html' (NOT 'body'), selectedUser is an ARRAY of user IDs, include attachments:[] and isCloned:false. A 'send_email' discriminator inside a 'notification' object saves but silently never sends.",
|
|
110
110
|
"emailChannelExample": {
|
|
111
111
|
"type": "email",
|
|
112
112
|
"email": {
|
|
@@ -166,6 +166,27 @@
|
|
|
166
166
|
},
|
|
167
167
|
"notes": "Requires BOTH workflowId (string) AND workflow_id (ARRAY with same ID). Also needs type: 'remove_from_workflow' inside attributes. Derived from GHL UI."
|
|
168
168
|
},
|
|
169
|
+
"internal_create_opportunity": {
|
|
170
|
+
"example": {
|
|
171
|
+
"type": "internal_create_opportunity",
|
|
172
|
+
"workflowsActionType": "INTERNAL",
|
|
173
|
+
"attributes": {
|
|
174
|
+
"type": "internal_create_opportunity",
|
|
175
|
+
"pipelineId": "PIPELINE_ID",
|
|
176
|
+
"__customInputFields__": [
|
|
177
|
+
{
|
|
178
|
+
"__customInputs__": {},
|
|
179
|
+
"dataType": "SINGLE_OPTIONS",
|
|
180
|
+
"filterField": "pipelineStageId",
|
|
181
|
+
"value": "STAGE_ID",
|
|
182
|
+
"valueFieldType": "select"
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
"__customInputs__": {}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"notes": "CREATES a card (GHL's newer separate 'Create Opportunity' action). This minimal shape is live-proven to create at runtime (2026-07-24): pipelineId at the ATTRIBUTES level (not inside __customInputFields__), __customInputFields__ carrying pipelineStageId, workflowsActionType:'INTERNAL' at the NODE level. UI-built nodes also carry name/status/monetaryValue entries and work \u2014 copy a working node field-for-field rather than inventing entries. Without a name entry the card is named from the contact (observed live 2026-07-24). Use THIS for 'create an opportunity for the new lead'."
|
|
189
|
+
},
|
|
169
190
|
"internal_update_opportunity": {
|
|
170
191
|
"example": {
|
|
171
192
|
"__customInputFields__": [
|
|
@@ -185,7 +206,7 @@
|
|
|
185
206
|
"workflowsActionType": "INTERNAL",
|
|
186
207
|
"type": "internal_update_opportunity"
|
|
187
208
|
},
|
|
188
|
-
"notes": "CREATABLE from scratch (re-enabled v3.41.0). The discriminator workflowsActionType:'INTERNAL' MUST sit at the NODE level, never nested in attributes \u2014 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) \u2014 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."
|
|
209
|
+
"notes": "GHL's combined 'Create/Update Opportunity'. With only pipelineId + pipelineStageId it CANNOT create a card \u2014 it moves one the contact already has; a contact with no opportunity (a new lead) gets nothing and the workflow continues as if it worked (customer report 2026-08-26). It creates only when __customInputFields__ also carry Opportunity name (GHL's doc: Name, Source, Status mandatory to create). GoHighLevel is phasing this combined action out for new workflows, so prefer internal_create_opportunity to create; use this node after find_opportunity, after a create step in the same workflow, or on an opportunity trigger. CREATABLE from scratch (re-enabled v3.41.0). The discriminator workflowsActionType:'INTERNAL' MUST sit at the NODE level, never nested in attributes \u2014 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) \u2014 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."
|
|
189
210
|
},
|
|
190
211
|
"_if_else_branching": {
|
|
191
212
|
"notes": "if_else is a node type discriminator only. Do not send a single flat if_else action.",
|