@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.
- package/CHANGELOG.md +53 -0
- package/dist/index.js +149 -20
- 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.
|
|
34
|
+
version: "3.31.0",
|
|
35
35
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
36
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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.
|
|
3
|
+
"version": "3.31.0",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
|
-
"description": "GoHighLevel MCP Server for Claude.
|
|
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"
|