@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 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 ID validation for ONE deployed GHL workflow. 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'.",
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, status: "ok", references_scanned: 0, issues_count: 0, warnings_count: 0, findings: [] });
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
- status: findings.some((f) => f.severity === "error") ? "issues_found" : "ok",
13469
+ ...summarizeFindings(findings),
13316
13470
  references_scanned: refs.length,
13317
- issues_count: findings.filter((f) => f.severity === "error").length,
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: 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.",
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.69.0",
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.69.0",
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 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.",
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.",