@elitedcs/ghl-mcp 3.40.0 → 3.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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.40.0",
34
+ version: "3.41.0",
35
35
  mcpName: "io.github.drjerryrelth/ghl-command",
36
36
  description: "GoHighLevel MCP Server for Claude. 218 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
37
37
  main: "dist/index.js",
@@ -705,6 +705,12 @@ function decodeFirebaseClaims(idToken) {
705
705
  }
706
706
  }
707
707
 
708
+ // src/id-shape.ts
709
+ var GHL_ID_SHAPE = /^([A-Za-z0-9]{17,}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
710
+ function isIdShaped(s) {
711
+ return typeof s === "string" && GHL_ID_SHAPE.test(s);
712
+ }
713
+
708
714
  // src/trigger-schemas.ts
709
715
  var import_zod3 = require("zod");
710
716
  var TriggerActionSchema = import_zod3.z.object({
@@ -1170,6 +1176,31 @@ function normalizeRemoveFromWorkflowAction(action) {
1170
1176
  }
1171
1177
  return { ...action, attributes: next };
1172
1178
  }
1179
+ function normalizeInternalUpdateOpportunityAction(action) {
1180
+ if (action.type !== "internal_update_opportunity") return action;
1181
+ const attrIn = action.attributes && typeof action.attributes === "object" ? action.attributes : {};
1182
+ const fieldsIn = Array.isArray(attrIn.__customInputFields__) ? attrIn.__customInputFields__ : [];
1183
+ const __customInputFields__ = fieldsIn.map((raw) => {
1184
+ const f = raw && typeof raw === "object" ? raw : {};
1185
+ return {
1186
+ ...f,
1187
+ // shape-preserving — keep every key a real node carries
1188
+ __customInputs__: f.__customInputs__ && typeof f.__customInputs__ === "object" ? f.__customInputs__ : {},
1189
+ dataType: typeof f.dataType === "string" ? f.dataType : "SINGLE_OPTIONS",
1190
+ valueFieldType: typeof f.valueFieldType === "string" ? f.valueFieldType : "select"
1191
+ };
1192
+ });
1193
+ const attributes = {
1194
+ ...attrIn,
1195
+ // shape-preserving
1196
+ type: "internal_update_opportunity",
1197
+ allowBackward: typeof attrIn.allowBackward === "boolean" ? attrIn.allowBackward : false,
1198
+ __customInputs__: attrIn.__customInputs__ && typeof attrIn.__customInputs__ === "object" ? attrIn.__customInputs__ : {},
1199
+ __customInputFields__
1200
+ };
1201
+ delete attributes.workflowsActionType;
1202
+ return { ...action, workflowsActionType: "INTERNAL", attributes };
1203
+ }
1173
1204
  function hasId(action) {
1174
1205
  return typeof action.id === "string" && action.id.length > 0;
1175
1206
  }
@@ -1185,7 +1216,7 @@ function getStringArray(value) {
1185
1216
  }
1186
1217
  return value;
1187
1218
  }
1188
- function validateActionChain(actions) {
1219
+ function validateActionChain(actions, existingIds) {
1189
1220
  const byId = new Map(actions.filter(hasId).map((action) => [action.id, action]));
1190
1221
  for (const action of actions) {
1191
1222
  const attr = isRecord(action.attributes) ? action.attributes : void 0;
@@ -1207,12 +1238,24 @@ function validateActionChain(actions) {
1207
1238
  if (!attr.startAfter) throw new Error(`Wait action "${action.name}" missing required 'startAfter' in attributes.`);
1208
1239
  break;
1209
1240
  case "internal_update_opportunity": {
1210
- 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
- );
1241
+ const isRoundTripped = hasId(action) && (existingIds ? existingIds.has(action.id) : true);
1242
+ if (!isRoundTripped) {
1243
+ const cif = Array.isArray(attr.__customInputFields__) ? attr.__customInputFields__ : null;
1244
+ if (!cif) {
1245
+ throw new Error(
1246
+ `Internal update opportunity action "${action.name}" missing '__customInputFields__' array (needs a pipelineId entry and a pipelineStageId entry). Use get_pipelines to find the IDs.`
1247
+ );
1248
+ }
1249
+ const target = (ff) => cif.find((f) => f && f.filterField === ff);
1250
+ for (const ff of ["pipelineId", "pipelineStageId"]) {
1251
+ const t = target(ff);
1252
+ if (!t || !isIdShaped(t.value)) {
1253
+ throw new Error(
1254
+ `Internal update opportunity action "${action.name}" needs a valid ${ff} entry (an id, not a name) in '__customInputFields__'. Use get_pipelines / list_pipelines_full to find the IDs. (A missing or non-existent id makes GHL silently fail this action and can kill the rest.)`
1255
+ );
1256
+ }
1257
+ }
1214
1258
  }
1215
- if (!Array.isArray(attr.__customInputFields__)) throw new Error(`Internal update opportunity action "${action.name}" missing '__customInputFields__' array.`);
1216
1259
  break;
1217
1260
  }
1218
1261
  case "remove_from_workflow":
@@ -1686,7 +1729,8 @@ ${errorBody}`
1686
1729
  }
1687
1730
  const currentActions = current.workflowData?.templates || [];
1688
1731
  const newActions = updates.actions ?? currentActions;
1689
- const linkedActions = this.buildActionChain(newActions);
1732
+ const existingActionIds = new Set(currentActions.filter(hasId).map((a) => a.id));
1733
+ const linkedActions = this.buildActionChain(newActions, existingActionIds);
1690
1734
  const currentIds = new Set(currentActions.filter(hasId).map((a) => a.id));
1691
1735
  const newIds = new Set(linkedActions.filter(hasId).map((a) => a.id));
1692
1736
  const createdSteps = linkedActions.filter(hasId).filter((a) => !currentIds.has(a.id)).map((a) => a.id);
@@ -1831,9 +1875,9 @@ ${errorBody}`
1831
1875
  * saves correctly but subsequent actions get skipped during execution.
1832
1876
  * Only use arrays for branching nodes (if/else) that point to multiple targets.
1833
1877
  */
1834
- buildActionChain(actions) {
1835
- validateActionChain(actions);
1836
- const linked = actions.map(normalizeRemoveFromWorkflowAction).map((action, i) => {
1878
+ buildActionChain(actions, existingIds) {
1879
+ validateActionChain(actions, existingIds);
1880
+ const linked = actions.map(normalizeRemoveFromWorkflowAction).map(normalizeInternalUpdateOpportunityAction).map((action, i) => {
1837
1881
  const copy = { ...action };
1838
1882
  if (!copy.id) {
1839
1883
  copy.id = crypto.randomUUID();
@@ -5512,7 +5556,7 @@ function registerWorkflowBuilderTools(server2, client) {
5512
5556
  );
5513
5557
  server2.tool(
5514
5558
  "update_workflow_actions",
5515
- "Update a workflow's actions (steps), triggers, name, or status. IMPORTANT: Call get_workflow_full first to see the current state before updating. Handles version tracking automatically. Uses the internal builder API (requires Firebase auth). Action types: sms, email, add_contact_tag, remove_contact_tag, wait, webhook, internal_update_opportunity, custom_code, update_contact_field, add_notes, internal_notification, task_notification, remove_from_workflow, add_to_workflow, goto, transition, workflow_goal. NOTE: internal_update_opportunity (GHL's 'Update Opportunity' / move-to-stage action) 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.",
5559
+ "Update a workflow's actions (steps), triggers, name, or status. IMPORTANT: Call get_workflow_full first to see the current state before updating. Handles version tracking automatically. Uses the internal builder API (requires Firebase auth). Action types: sms, email, add_contact_tag, remove_contact_tag, wait, webhook, internal_update_opportunity, custom_code, update_contact_field, add_notes, internal_notification, task_notification, remove_from_workflow, add_to_workflow, goto, transition, workflow_goal. NOTE: internal_update_opportunity (GHL's 'Create/Update Opportunity' / move-to-stage action) IS creatable from scratch \u2014 this tool normalizes it to GHL's exact node shape (workflowsActionType:'INTERNAL' hoisted to the NODE level; a nested copy is what makes GHL reject the node as 'action has a corrupted type'). A synthesized node needs both a pipelineId and a pipelineStageId entry in attributes.__customInputFields__ (use get_pipelines for the IDs); a node round-tripped via get_workflow_full keeps its id and passes through unchanged. For if/else, call build_if_else_branch and include its returned nodes; if_else is a node discriminator, not a standalone action. For goal events (exit-on-condition nodes), call build_goal_event to get a correctly-shaped workflow_goal node \u2014 wire it in by setting the prior action's `next` to the goal node's id. Trigger types: all 57 native GHL trigger types have typed validation; any unknown trigger type passes through via a permissive fallback so reads never crash.",
5516
5560
  {
5517
5561
  workflowId: import_zod33.z.string().describe("The workflow ID to update."),
5518
5562
  name: import_zod33.z.string().optional().describe("New workflow name."),
@@ -8667,10 +8711,6 @@ ${errors.join("\n")}` : "\nNo errors!",
8667
8711
  // src/tools/validators.ts
8668
8712
  var import_zod47 = require("zod");
8669
8713
  var ALL_CATEGORIES = ["pipeline", "stage", "custom_field", "user", "workflow", "form", "calendar", "survey"];
8670
- var ID_SHAPE = /^([A-Za-z0-9]{17,}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
8671
- function isIdShaped(s) {
8672
- return typeof s === "string" && ID_SHAPE.test(s);
8673
- }
8674
8714
  var STANDARD_CONTACT_FIELDS = /* @__PURE__ */ new Set([
8675
8715
  "first_name",
8676
8716
  "firstname",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.40.0",
3
+ "version": "3.41.0",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
5
  "description": "GoHighLevel MCP Server for Claude. 218 tools — full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",
@@ -165,7 +165,7 @@
165
165
  "workflowsActionType": "INTERNAL",
166
166
  "type": "internal_update_opportunity"
167
167
  },
168
- "notes": "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."
168
+ "notes": "CREATABLE from scratch (re-enabled v3.41.0). The discriminator workflowsActionType:'INTERNAL' MUST sit at the NODE level, never nested in attributes — a nested copy makes GHL reject the node as 'action has a corrupted type' and silently fail the whole save. update_workflow_actions normalizes this for you (hoists workflowsActionType to the node level, scaffolds allowBackward + __customInputs__, gives each __customInputFields__ entry an __customInputs__). The shape below (workflowsActionType at the node level alongside type/name/attributes) is correct for both creating and round-tripping. Use pipeline and stage IDs (not names) get_pipelines / list_pipelines_full to find them FIRST. CRITICAL: if the pipelineId or pipelineStageId don't exist in the target sub-account, GHL silently fails this action AND can kill subsequent actions. A synthesized node needs BOTH a pipelineId and a pipelineStageId entry; a node round-tripped via get_workflow_full keeps its id and passes through unchanged."
169
169
  },
170
170
 
171
171
  "_if_else_branching": {