@elitedcs/ghl-mcp 3.29.0 → 3.31.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/index.js +149 -20
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.31.0 — Snapshots: list + share-link (agency tooling)
4
+
5
+ Two new agency-level tools, the first build toward removing manual steps from the
6
+ client-provisioning runbook (snapshot selection + handoff for new sub-accounts).
7
+
8
+ - **`list_snapshots`** — list the agency's snapshots (`id`, `name`, `type`) so you
9
+ can pick the right one by name. Read-only.
10
+ - **`create_snapshot_share_link`** — mint a load/share link for a snapshot
11
+ (`gohighlevel.com/?share=…`) to import it into a sub-account. Requires an explicit
12
+ `share_type` (`link`, `permanent_link`, `agency_link`, `location_link`,
13
+ `marketplace_link`).
14
+
15
+ Both use the agency/company-scoped key (`getAgencyKey()`), not a sub-account PIT —
16
+ snapshots are an agency resource. The tools resolve `companyId` with strict rules
17
+ (explicit param > active location's company; mismatch is rejected, not guessed) so
18
+ a multi-tenant install can't list the wrong agency's snapshots, and they fail with
19
+ an actionable message when no agency key is registered.
20
+
21
+ **Notes / limits (verified against the live API):**
22
+ - Creating a sub-account from a snapshot is NOT exposed: `POST /locations/` returns
23
+ 401 for PIT auth. Snapshot *apply* stays a guided GHL-UI step.
24
+ - `create_snapshot_share_link` is not idempotent (each call mints a new link, no
25
+ revoke API — remove in the UI). The HTTP client gained an internal `noRetry`
26
+ option so a lost-response retry can't silently create a duplicate.
27
+
28
+ ## 3.30.0 — Workflow trigger stays active after edit/publish (Bug 7)
29
+
30
+ Editing a trigger on a published workflow via `update_workflow_actions` silently
31
+ disabled it (`active` flipped to `false`), and there was no MCP path to turn it
32
+ back on. Any contract/tag/form trigger you edited stopped firing.
33
+
34
+ **Root cause.** The internal trigger-write helper hardcoded `status: "draft"` on
35
+ every trigger POST/PUT. GHL derives a trigger's stored `active` flag from that
36
+ write-time `status`. Verified live against the sandbox:
37
+
38
+ - `status:"published"` → `active:true`
39
+ - `status:"draft"` → `active:false` (and so do `"active"`, `"live"`, and
40
+ omitting `status` entirely)
41
+
42
+ So every trigger edit re-drafted the trigger, and nothing flipped it back —
43
+ `publish_workflow` didn't either, because it sent no triggers at all.
44
+
45
+ **Fix.** Trigger writes now mirror the workflow's own published/draft state:
46
+
47
+ - `update_workflow_actions` on a published workflow writes its triggers as
48
+ `published`, so editing a trigger condition keeps it live.
49
+ - `publish_workflow` now re-syncs the workflow's triggers, so a draft →
50
+ published transition (including the documented create → add-trigger →
51
+ publish flow) activates them.
52
+
53
+ Action-chain linking and the workflow-setting preservation from 3.29.0 are
54
+ unchanged.
55
+
3
56
  ## 3.29.0 — Documents & Contracts API fix + workflow/contact bug fixes
4
57
 
5
58
  Five confirmed bugs fixed, verified live against the GHL API.
package/dist/index.js CHANGED
@@ -31,9 +31,9 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@elitedcs/ghl-mcp",
34
- version: "3.29.0",
34
+ version: "3.31.0",
35
35
  mcpName: "io.github.drjerryrelth/ghl-command",
36
- description: "GoHighLevel MCP Server for Claude. 212 tools \u2014 full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
36
+ description: "GoHighLevel MCP Server for Claude. 214 tools \u2014 full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
37
37
  main: "dist/index.js",
38
38
  bin: {
39
39
  "ghl-mcp": "dist/index.js"
@@ -203,7 +203,7 @@ var GHLClient = class {
203
203
  if (error instanceof Error && error.name === "AbortError") {
204
204
  throw new Error(`Request timeout (30s): ${method} ${path6}`);
205
205
  }
206
- if (attempt < MAX_RETRIES) {
206
+ if (!options.noRetry && attempt < MAX_RETRIES) {
207
207
  const delay4 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
208
208
  process.stderr.write(`[ghl-mcp] Network error on ${method} ${path6}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
209
209
  `);
@@ -214,7 +214,7 @@ var GHLClient = class {
214
214
  } finally {
215
215
  clearTimeout(timeout);
216
216
  }
217
- if ((response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
217
+ if (!options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
218
218
  const delay4 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
219
219
  process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path6}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
220
220
  `);
@@ -1636,8 +1636,9 @@ ${errorBody}`
1636
1636
  */
1637
1637
  async updateWorkflow(workflowId, updates) {
1638
1638
  const current = await this.getWorkflow(workflowId);
1639
+ const effectiveStatus = updates.status ?? current.status;
1639
1640
  if (updates.triggers !== void 0) {
1640
- await this.syncTriggers(workflowId, current.triggers || [], updates.triggers);
1641
+ await this.syncTriggers(workflowId, current.triggers || [], updates.triggers, effectiveStatus);
1641
1642
  }
1642
1643
  const currentActions = current.workflowData?.templates || [];
1643
1644
  const newActions = updates.actions ?? currentActions;
@@ -1649,7 +1650,7 @@ ${errorBody}`
1649
1650
  const body = {
1650
1651
  name: updates.name ?? current.name,
1651
1652
  isRestoreRequest: true,
1652
- status: updates.status ?? current.status,
1653
+ status: effectiveStatus,
1653
1654
  version: current.version,
1654
1655
  dataVersion: current.dataVersion ?? 1,
1655
1656
  timezone: current.timezone ?? "account",
@@ -1682,16 +1683,30 @@ ${errorBody}`
1682
1683
  return this.request("DELETE", `/${this.locationId}/${workflowId}`);
1683
1684
  }
1684
1685
  /**
1685
- * Publish a draft workflow
1686
+ * Publish a draft workflow.
1687
+ *
1688
+ * Publishing must also re-activate the workflow's triggers. GHL stores a
1689
+ * trigger's `active` flag based on the write-time `status`, so triggers
1690
+ * created/edited while the workflow was a draft are written as "draft"
1691
+ * (active:false). Re-syncing them here under the now-"published" status
1692
+ * flips them live — otherwise a freshly published workflow's triggers never
1693
+ * fire (and the create → update_workflow_actions → publish_workflow flow
1694
+ * would leave new triggers inactive).
1686
1695
  */
1687
1696
  async publishWorkflow(workflowId) {
1688
- return this.updateWorkflow(workflowId, { status: "published" });
1697
+ const current = await this.getWorkflow(workflowId);
1698
+ return this.updateWorkflow(workflowId, {
1699
+ status: "published",
1700
+ triggers: current.triggers ?? []
1701
+ });
1689
1702
  }
1690
1703
  /**
1691
1704
  * Create a workflow trigger. GHL generates the Firestore id and returns it.
1705
+ * `workflowStatus` is the owning workflow's published/draft state; it
1706
+ * determines whether the new trigger is written live or as a draft.
1692
1707
  */
1693
- async createTrigger(workflowId, trigger) {
1694
- const payload = this.buildTriggerPayload(workflowId, trigger);
1708
+ async createTrigger(workflowId, trigger, workflowStatus) {
1709
+ const payload = this.buildTriggerPayload(workflowId, trigger, workflowStatus);
1695
1710
  const raw = await this.request("POST", `/${this.locationId}/trigger`, payload);
1696
1711
  if (isRecord(raw) && typeof raw.id === "string") {
1697
1712
  return raw.id;
@@ -1699,10 +1714,11 @@ ${errorBody}`
1699
1714
  throw new Error(`Trigger creation did not return an id. Response: ${JSON.stringify(raw)}`);
1700
1715
  }
1701
1716
  /**
1702
- * Update an existing workflow trigger.
1717
+ * Update an existing workflow trigger. `workflowStatus` is the owning
1718
+ * workflow's published/draft state (see buildTriggerPayload).
1703
1719
  */
1704
- async updateTrigger(workflowId, trigger) {
1705
- const payload = this.buildTriggerPayload(workflowId, trigger);
1720
+ async updateTrigger(workflowId, trigger, workflowStatus) {
1721
+ const payload = this.buildTriggerPayload(workflowId, trigger, workflowStatus);
1706
1722
  return this.request("PUT", `/${this.locationId}/trigger/${trigger.id}`, payload);
1707
1723
  }
1708
1724
  /**
@@ -1716,7 +1732,7 @@ ${errorBody}`
1716
1732
  * Triggers without an id are created; triggers present in current but
1717
1733
  * missing from desired are deleted; triggers in both are updated in place.
1718
1734
  */
1719
- async syncTriggers(workflowId, current, desired) {
1735
+ async syncTriggers(workflowId, current, desired, workflowStatus) {
1720
1736
  const currentById = /* @__PURE__ */ new Map();
1721
1737
  for (const t of current) {
1722
1738
  if (typeof t.id === "string") currentById.set(t.id, t);
@@ -1732,9 +1748,9 @@ ${errorBody}`
1732
1748
  }
1733
1749
  for (const trigger of desired) {
1734
1750
  if (hasTriggerId(trigger) && currentById.has(trigger.id)) {
1735
- await this.updateTrigger(workflowId, trigger);
1751
+ await this.updateTrigger(workflowId, trigger, workflowStatus);
1736
1752
  } else {
1737
- await this.createTrigger(workflowId, trigger);
1753
+ await this.createTrigger(workflowId, trigger, workflowStatus);
1738
1754
  }
1739
1755
  }
1740
1756
  }
@@ -1742,7 +1758,8 @@ ${errorBody}`
1742
1758
  * Shape a trigger for the POST/PUT trigger endpoints, matching what the
1743
1759
  * GHL UI sends. Missing fields are filled from the client's context.
1744
1760
  */
1745
- buildTriggerPayload(workflowId, trigger) {
1761
+ buildTriggerPayload(workflowId, trigger, workflowStatus) {
1762
+ const isPublished = workflowStatus === "published";
1746
1763
  return {
1747
1764
  ...trigger,
1748
1765
  workflowId,
@@ -1752,8 +1769,8 @@ ${errorBody}`
1752
1769
  belongs_to: "workflow",
1753
1770
  actions: trigger.actions ?? [{ workflow_id: workflowId, type: "add_to_workflow" }],
1754
1771
  schedule_config: trigger.schedule_config ?? {},
1755
- active: trigger.active ?? true,
1756
- status: "draft"
1772
+ active: isPublished,
1773
+ status: isPublished ? "published" : "draft"
1757
1774
  };
1758
1775
  }
1759
1776
  /**
@@ -8800,6 +8817,115 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
8800
8817
  );
8801
8818
  }
8802
8819
 
8820
+ // src/tools/snapshots.ts
8821
+ var import_zod48 = require("zod");
8822
+ var SnapshotSchema = import_zod48.z.object({ id: import_zod48.z.string(), name: import_zod48.z.string(), type: import_zod48.z.string() }).passthrough();
8823
+ var SnapshotsResponseSchema = import_zod48.z.object({ snapshots: import_zod48.z.array(SnapshotSchema) }).passthrough();
8824
+ var ShareLinkResponseSchema = import_zod48.z.object({ id: import_zod48.z.string(), shareLink: import_zod48.z.string() }).passthrough();
8825
+ var SHARE_TYPES = [
8826
+ "link",
8827
+ "permanent_link",
8828
+ "agency_link",
8829
+ "location_link",
8830
+ "marketplace_link"
8831
+ ];
8832
+ function resolveSnapshotAuth(client, registry2, companyIdParam) {
8833
+ const agencyKey = registry2?.getAgencyKey();
8834
+ if (!agencyKey) {
8835
+ throw new Error(
8836
+ "Snapshots require an agency/company-scoped API key, and none is registered. This install only has sub-account Private Integration keys. Add the agency key (created at the AGENCY level in GHL Settings > Integrations) to the token registry, then retry."
8837
+ );
8838
+ }
8839
+ const storedCompanyId = client.defaultLocationId ? registry2?.getToken(client.defaultLocationId)?.companyId : void 0;
8840
+ const param = companyIdParam?.trim() || void 0;
8841
+ if (param && storedCompanyId && param !== storedCompanyId) {
8842
+ throw new Error(
8843
+ `companyId mismatch: you passed ${param}, but the active location belongs to company ${storedCompanyId}. Refusing to guess. Switch to a location in the intended company, or pass the matching companyId.`
8844
+ );
8845
+ }
8846
+ const companyId = param ?? storedCompanyId;
8847
+ if (!companyId) {
8848
+ throw new Error(
8849
+ "Could not determine the companyId for this snapshot call. The active location has no stored company. Pass companyId explicitly, or run register_location / switch_location for the active location so its company is recorded."
8850
+ );
8851
+ }
8852
+ return { agencyClient: new GHLClient({ apiKey: agencyKey }), companyId };
8853
+ }
8854
+ function explainSnapshotError(error, companyId) {
8855
+ const message = error instanceof Error ? error.message : String(error);
8856
+ if (message.includes("401") || message.includes("Unauthorized") || message.includes("403")) {
8857
+ throw new Error(
8858
+ `${message}
8859
+
8860
+ The agency key is scoped to a single company and may not cover company ${companyId}. Snapshots only list for the agency that owns the key. Use a companyId belonging to the agency key, or register that agency's own key.`
8861
+ );
8862
+ }
8863
+ throw error instanceof Error ? error : new Error(message);
8864
+ }
8865
+ function registerSnapshotTools(server2, client, registry2) {
8866
+ safeTool(
8867
+ server2,
8868
+ "list_snapshots",
8869
+ "List the agency's GHL snapshots (id, name, type) so you can pick the right one by name before applying it to a sub-account. Reads the agency/company-scoped key (not a sub-account PIT). companyId defaults to the active location's company; pass it explicitly to target a different company you have the agency key for. Read-only.",
8870
+ {
8871
+ companyId: import_zod48.z.string().optional().describe(
8872
+ "Agency/company ID whose snapshots to list. Defaults to the active location's company. Must match the company your agency key is scoped to."
8873
+ )
8874
+ },
8875
+ async ({ companyId }) => {
8876
+ const { agencyClient, companyId: resolved } = resolveSnapshotAuth(client, registry2, companyId);
8877
+ let raw;
8878
+ try {
8879
+ raw = await agencyClient.get("/snapshots/", { params: { companyId: resolved } });
8880
+ } catch (error) {
8881
+ explainSnapshotError(error, resolved);
8882
+ }
8883
+ const parsed = SnapshotsResponseSchema.parse(raw);
8884
+ return {
8885
+ companyId: resolved,
8886
+ count: parsed.snapshots.length,
8887
+ snapshots: parsed.snapshots
8888
+ };
8889
+ }
8890
+ );
8891
+ safeTool(
8892
+ server2,
8893
+ "create_snapshot_share_link",
8894
+ "Create a shareable load link for one of the agency's snapshots (returns a gohighlevel.com/?share=... URL to import the snapshot into a sub-account). Uses the agency/company-scoped key. WARNING: this is NOT idempotent \u2014 each call mints a NEW link, and there is no API to list or revoke links (revoke in the GHL UI under the snapshot's share settings). Pick share_type deliberately. Use list_snapshots first to get the snapshot id.",
8895
+ {
8896
+ snapshot_id: import_zod48.z.string().describe("The snapshot id to share (from list_snapshots)."),
8897
+ share_type: import_zod48.z.enum(SHARE_TYPES).describe(
8898
+ "Share link type. 'link' = standard share link; 'permanent_link' = non-expiring; 'agency_link' = share to agencies; 'location_link' = load into a sub-account/location; 'marketplace_link' = marketplace listing. No default \u2014 choose intentionally."
8899
+ ),
8900
+ companyId: import_zod48.z.string().optional().describe(
8901
+ "Agency/company ID that owns the snapshot. Defaults to the active location's company. Must match the company your agency key is scoped to."
8902
+ )
8903
+ },
8904
+ async ({ snapshot_id, share_type, companyId }) => {
8905
+ const { agencyClient, companyId: resolved } = resolveSnapshotAuth(client, registry2, companyId);
8906
+ let raw;
8907
+ try {
8908
+ raw = await agencyClient.post("/snapshots/share/link", {
8909
+ params: { companyId: resolved },
8910
+ body: { snapshot_id, share_type },
8911
+ noRetry: true
8912
+ });
8913
+ } catch (error) {
8914
+ explainSnapshotError(error, resolved);
8915
+ }
8916
+ const parsed = ShareLinkResponseSchema.parse(raw);
8917
+ return {
8918
+ companyId: resolved,
8919
+ snapshot_id,
8920
+ share_type,
8921
+ id: parsed.id,
8922
+ shareLink: parsed.shareLink,
8923
+ _note: "A new share link was created. There is no API to revoke it \u2014 remove it in the GHL UI (snapshot share settings) if needed."
8924
+ };
8925
+ }
8926
+ );
8927
+ }
8928
+
8803
8929
  // src/tools/index.ts
8804
8930
  var publicApiTools = [
8805
8931
  [registerContactTools, "contacts"],
@@ -8848,12 +8974,14 @@ var internalApiTools = [
8848
8974
  var VALIDATORS_MODULE = "validators";
8849
8975
  var DIAGNOSTICS_MODULE = "diagnostics";
8850
8976
  var LOCATION_SWITCHER_MODULE = "location-switcher";
8977
+ var SNAPSHOTS_MODULE = "snapshots";
8851
8978
  var KNOWN_MODULES = /* @__PURE__ */ new Set([
8852
8979
  ...publicApiTools.map(([, label]) => label),
8853
8980
  ...internalApiTools.map(([, label]) => label),
8854
8981
  VALIDATORS_MODULE,
8855
8982
  DIAGNOSTICS_MODULE,
8856
- LOCATION_SWITCHER_MODULE
8983
+ LOCATION_SWITCHER_MODULE,
8984
+ SNAPSHOTS_MODULE
8857
8985
  ]);
8858
8986
  function registerAllTools(server2, client, registry2, mcpVersion, env = process.env) {
8859
8987
  const config3 = parseAllowlist(env);
@@ -8875,6 +9003,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
8875
9003
  builderClient,
8876
9004
  registry2 ?? null
8877
9005
  );
9006
+ registerSnapshotTools(wrap(SNAPSHOTS_MODULE), client, registry2);
8878
9007
  registerLocationSwitcherTools(
8879
9008
  wrap(LOCATION_SWITCHER_MODULE),
8880
9009
  client,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.29.0",
3
+ "version": "3.31.0",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
- "description": "GoHighLevel MCP Server for Claude. 212 tools — full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
5
+ "description": "GoHighLevel MCP Server for Claude. 214 tools — full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "ghl-mcp": "dist/index.js"