@elitedcs/ghl-mcp 3.30.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
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
+
3
28
  ## 3.30.0 — Workflow trigger stays active after edit/publish (Bug 7)
4
29
 
5
30
  Editing a trigger on a published workflow via `update_workflow_actions` silently
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.30.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
  `);
@@ -8817,6 +8817,115 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
8817
8817
  );
8818
8818
  }
8819
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
+
8820
8929
  // src/tools/index.ts
8821
8930
  var publicApiTools = [
8822
8931
  [registerContactTools, "contacts"],
@@ -8865,12 +8974,14 @@ var internalApiTools = [
8865
8974
  var VALIDATORS_MODULE = "validators";
8866
8975
  var DIAGNOSTICS_MODULE = "diagnostics";
8867
8976
  var LOCATION_SWITCHER_MODULE = "location-switcher";
8977
+ var SNAPSHOTS_MODULE = "snapshots";
8868
8978
  var KNOWN_MODULES = /* @__PURE__ */ new Set([
8869
8979
  ...publicApiTools.map(([, label]) => label),
8870
8980
  ...internalApiTools.map(([, label]) => label),
8871
8981
  VALIDATORS_MODULE,
8872
8982
  DIAGNOSTICS_MODULE,
8873
- LOCATION_SWITCHER_MODULE
8983
+ LOCATION_SWITCHER_MODULE,
8984
+ SNAPSHOTS_MODULE
8874
8985
  ]);
8875
8986
  function registerAllTools(server2, client, registry2, mcpVersion, env = process.env) {
8876
8987
  const config3 = parseAllowlist(env);
@@ -8892,6 +9003,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
8892
9003
  builderClient,
8893
9004
  registry2 ?? null
8894
9005
  );
9006
+ registerSnapshotTools(wrap(SNAPSHOTS_MODULE), client, registry2);
8895
9007
  registerLocationSwitcherTools(
8896
9008
  wrap(LOCATION_SWITCHER_MODULE),
8897
9009
  client,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.30.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"