@elitedcs/ghl-mcp 3.49.1 → 3.52.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 +92 -0
- package/README.md +6 -4
- package/dist/index.js +325 -29
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,97 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.52.0 — Fix: `update_page_content` now actually writes page content
|
|
4
|
+
|
|
5
|
+
`update_page_content` was a silent no-op — it POSTed the page envelope to GHL's
|
|
6
|
+
prebuilt-section template-sync route (`/funnels/builder/prebuilt-section/sync/changes`),
|
|
7
|
+
which accepts the request, ignores `pageData`, returns `{"prebuiltSectionTemplates":[]}`,
|
|
8
|
+
and persists nothing (the page stayed at version 1 with no `pageDataDownloadUrl`).
|
|
9
|
+
Present since v3.7.0 — this capability had never worked. Reported by a customer
|
|
10
|
+
(v3.47 + v3.50, custom MCP client) and reproduced.
|
|
11
|
+
|
|
12
|
+
- **Re-pointed to GHL's real builder autosave endpoint**
|
|
13
|
+
`POST /funnels/builder/autosave/{pageId}`, captured live from the page builder
|
|
14
|
+
2026-07-09. The server uploads the page to Firebase Storage, bumps the version,
|
|
15
|
+
and regenerates the preview snapshot, returning the new `pageDataUrl` /
|
|
16
|
+
`pageDataDownloadUrl`. Verified end-to-end: a from-scratch API write bumps the
|
|
17
|
+
version and the content round-trips through `get_page_content`.
|
|
18
|
+
- **Body shape** is `{funnelId, pageVersion, pageData: {…envelope, pageVersion,
|
|
19
|
+
pageType, manualSave, integrations}}`. The tool auto-resolves `funnelId` and the
|
|
20
|
+
current `pageVersion` from the page metadata (both optional to pass), computes
|
|
21
|
+
the `integrations` element-type counts from the content, and sends the builder
|
|
22
|
+
origin/referer + `Version` header the endpoint's IAM check requires.
|
|
23
|
+
- `update_page_content` gains an optional `funnelId` arg (auto-resolved when
|
|
24
|
+
omitted). No tool-count change.
|
|
25
|
+
|
|
26
|
+
## 3.51.0 — Agency sub-account provisioning (`create_sub_account` / `delete_sub_account`)
|
|
27
|
+
|
|
28
|
+
The missing first step of fully programmatic client provisioning: create the
|
|
29
|
+
sub-account itself (optionally loading a snapshot at creation), then Blueprint
|
|
30
|
+
builds inside it and verify_funnel proves it.
|
|
31
|
+
|
|
32
|
+
- **`create_sub_account`** — agency-API location create with optional
|
|
33
|
+
`snapshot_id` (see `list_snapshots`), prospect/owner info, address, timezone.
|
|
34
|
+
Probes whether the agency key can operate the new location's data plane and
|
|
35
|
+
auto-registers it in the token registry when it can (switch_location works
|
|
36
|
+
immediately). Non-idempotent create is retry-suppressed. Requires an
|
|
37
|
+
agency-level key with the locations.write scope; the error path says exactly
|
|
38
|
+
that when the scope is missing.
|
|
39
|
+
- **`delete_sub_account`** — agency-API location delete, confirm-gated,
|
|
40
|
+
refuses the currently active location, cleans the token registry entry.
|
|
41
|
+
- **Tool counts corrected: the real full count is 232 (180 core), free tier
|
|
42
|
+
106 (88).** A live tools/list dump exposed that `get_mcp_version` registers
|
|
43
|
+
outside the counted registry in both modes — every prior claim (229, 231,
|
|
44
|
+
free 105/87) was one short. The drift-guard constant now documents all four
|
|
45
|
+
normal-mode extras.
|
|
46
|
+
- **Test-isolation fix: the suite can never clobber a real install again.**
|
|
47
|
+
credentials-store's atomicity tests wrote fixture credentials to the REAL
|
|
48
|
+
per-user credentials.json (no GHL_MCP_CONFIG_DIR override) — any dev running
|
|
49
|
+
the suite silently broke their own install on next restart. Tests now run in
|
|
50
|
+
a temp dir AND writeCredentials refuses to run under vitest without an
|
|
51
|
+
override.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## 3.50.0 — FREE read-only tier
|
|
55
|
+
|
|
56
|
+
A free license (`ghlcommand.com/free`, key by email, no card) now runs the same
|
|
57
|
+
npm package in read-only mode on the user's own GHL account.
|
|
58
|
+
|
|
59
|
+
- **Tier gating in the tool registry** (`tool-filter.ts`, second axis alongside
|
|
60
|
+
the env allowlist). Free tier: every read-only tool works for real — all
|
|
61
|
+
`get_*`/`list_*`/`search_*` plus `audit_workflows`, `validate_workflow`,
|
|
62
|
+
`get_account_health_summary`, `compare_locations`, `verify_funnel`,
|
|
63
|
+
`check_blog_slug`, `workflow_builder_status`. That's **105 usable tools with
|
|
64
|
+
the Firebase login (87 before it)**, enumerated from the registry by the
|
|
65
|
+
drift-guard test. Write tools still register with full schemas but their
|
|
66
|
+
handlers return a clean upgrade message (checkout link, same-key upgrade
|
|
67
|
+
note) — never a stack trace, never a half-executed write. Excluded from free
|
|
68
|
+
by design: `export_account`, the whole Blueprint (`intake-to-build`) module,
|
|
69
|
+
and the multi-location registry writes (`register_location`,
|
|
70
|
+
`register_agency_key`, `switch_location`, …) — free is 1 location, 1 machine.
|
|
71
|
+
- **Tier is cryptographically bound.** The license server now signs `tier`
|
|
72
|
+
inside the Ed25519 attestation payload; the MCP derives the session tier ONLY
|
|
73
|
+
from a verified attestation (legacy attestations without the field = 'full').
|
|
74
|
+
Hand-editing credentials.json cannot unlock writes. Old MCP versions ignore
|
|
75
|
+
the extra payload field — fully backward compatible.
|
|
76
|
+
- **Firebase capture stays available on free** (`capture_firebase_interactive`,
|
|
77
|
+
`enable_workflow_builder`, `auto_capture_firebase_script`): the one-time GHL
|
|
78
|
+
login is what powers the read-only auditor suite. Builder writes stay gated.
|
|
79
|
+
- **setup_ghl_mcp / request_license** are tier-aware: free installs see the
|
|
80
|
+
read-only counts and the auditor-unlock next step; request_license now offers
|
|
81
|
+
the free tier (`ghlcommand.com/free`) alongside purchase.
|
|
82
|
+
- **Drift guard extended** (`tool-count.test.ts`): free-tier counts (105/87)
|
|
83
|
+
are computed from the registry and asserted against README + setup-tool copy;
|
|
84
|
+
a classification change breaks the build until the claims are re-synced. The
|
|
85
|
+
ghlcommand.com `/free` page and the key-delivery email carry the same number —
|
|
86
|
+
re-sync those manually when this test moves (different repos).
|
|
87
|
+
- Server side (elite-dcs/website + Supabase, deployed separately): `licenses.status`
|
|
88
|
+
gains `'free'`; `register_device` returns the row status; `validate-license`
|
|
89
|
+
returns + signs `tier`; the Stripe webhook upgrades a free row IN PLACE on
|
|
90
|
+
purchase (same key, installs 1→3, `source: stripe_97mo_from_free`) — proven
|
|
91
|
+
against a live throwaway row and a mocked-fetch webhook harness. New
|
|
92
|
+
`/api/free-license` provisioning endpoint (signup → row → key email → 3NA51
|
|
93
|
+
tag `ghl-command-free-tier`).
|
|
94
|
+
|
|
3
95
|
## 3.49.1 — Docs patch: one-click capture is now the primary Milestone-2 path
|
|
4
96
|
|
|
5
97
|
No behavior changes; text only.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GHL Command — GoHighLevel MCP Server
|
|
2
2
|
|
|
3
|
-
**Full GoHighLevel API access for Claude.**
|
|
3
|
+
**Full GoHighLevel API access for Claude.** 232 tools across 48 modules — manage contacts, conversations, pipelines, calendars, funnels, workflows, invoices, custom objects, webhooks, and more. **Includes full workflow builder, funnel/page editor, form builder, pipeline builder, bulk operations, account export, and workflow cloning** — capabilities no other GHL tool offers. **Multi-tenant:** one install can run the workflow builder across multiple clients' GHL accounts.
|
|
4
4
|
|
|
5
5
|
**Distributed via npm as [`@elitedcs/ghl-mcp`](https://www.npmjs.com/package/@elitedcs/ghl-mcp).** Buyers install with one config block — no git, no Node.js setup, no terminal commands. Updates flow automatically (`npx @latest` re-resolves on every Claude restart).
|
|
6
6
|
|
|
@@ -8,6 +8,8 @@ Works with **both the Claude Desktop App and Claude Code terminal** — your cho
|
|
|
8
8
|
|
|
9
9
|
**License required.** Get it at [ghlcommand.com](https://ghlcommand.com?utm_source=npm&utm_medium=readme) — $97/mo, covering UNLIMITED GoHighLevel sub-accounts on up to 3 machines (never billed per account). **30-day time-back guarantee**: save 5+ hours on one client build or your first month back.
|
|
10
10
|
|
|
11
|
+
**Try it FREE (read-only tier).** Grab an instant free key at [ghlcommand.com/free](https://ghlcommand.com/free?utm_source=npm&utm_medium=readme) — 106 read-only tools on your own GHL account (88 before the one-login auditor unlock): the account-wide workflow audit (`audit_workflows` finds the silently broken references GHL never warns you about), account health, and every read across contacts, pipelines, funnels, and workflows. Write tools stay visible and answer with upgrade info. Buying later with the same email upgrades your key in place — 1 machine on free, 3 when you upgrade.
|
|
12
|
+
|
|
11
13
|
Built by [Elite DCs, LLC](https://elitedcs.com).
|
|
12
14
|
|
|
13
15
|
---
|
|
@@ -116,7 +118,7 @@ Run setup_ghl_mcp to activate GHL Command:
|
|
|
116
118
|
ghl_location_id: YOUR_LOCATION_ID
|
|
117
119
|
```
|
|
118
120
|
|
|
119
|
-
Approve the tool call. Server validates your license, verifies your GHL credentials, writes them to a per-user config file. **Quit Claude one more time and reopen** — the full core toolset is now unlocked (
|
|
121
|
+
Approve the tool call. Server validates your license, verifies your GHL credentials, writes them to a per-user config file. **Quit Claude one more time and reopen** — the full core toolset is now unlocked (232 tools total with the optional Workflow Builder Firebase add-on).
|
|
120
122
|
|
|
121
123
|
### 4. Try it
|
|
122
124
|
|
|
@@ -156,7 +158,7 @@ https://app.gohighlevel.com/v2/location/YOUR_LOCATION_ID/dashboard
|
|
|
156
158
|
|
|
157
159
|
## Enable the Workflow Builder
|
|
158
160
|
|
|
159
|
-
The builder + cloner + validator tools (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, validate_workflow) use GHL's internal API and require Firebase credentials. Without them, the other
|
|
161
|
+
The builder + cloner + validator tools (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, validate_workflow) use GHL's internal API and require Firebase credentials. Without them, the other 180 tools work fine — you just won't have workflow/funnel/form/pipeline editing. This is the flagship feature: do this step.
|
|
160
162
|
|
|
161
163
|
**One-click capture (v3.49.0+, the supported path):**
|
|
162
164
|
|
|
@@ -745,7 +747,7 @@ Source repo is private. Contributors need an invitation from `drjerryrelth`. The
|
|
|
745
747
|
|
|
746
748
|
### Reducing context / token usage
|
|
747
749
|
|
|
748
|
-
Every registered MCP tool's schema is shipped to the model on every message. With
|
|
750
|
+
Every registered MCP tool's schema is shipped to the model on every message. With 232 tools that's a meaningful per-message context cost even in chats that never touch GHL. If you only use a slice of GHL Command, restrict the tool surface with `GHL_ENABLED_MODULES` and/or `GHL_ENABLED_TOOLS`:
|
|
749
751
|
|
|
750
752
|
```jsonc
|
|
751
753
|
// Claude Desktop config — enable whole modules
|
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.52.0",
|
|
35
35
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
36
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
36
|
+
description: "GoHighLevel MCP Server for Claude. 232 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.",
|
|
37
37
|
main: "dist/index.js",
|
|
38
38
|
bin: {
|
|
39
39
|
"ghl-mcp": "dist/index.js"
|
|
@@ -370,6 +370,11 @@ function foldCredentialsIntoEnv(creds, env = process.env) {
|
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
function writeCredentials(creds) {
|
|
373
|
+
if (process.env.VITEST && !process.env.GHL_MCP_CONFIG_DIR) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
"writeCredentials under vitest without GHL_MCP_CONFIG_DIR \u2014 set it to a temp dir in the test (see interactive-capture.test.ts) so the real credentials.json is never touched."
|
|
376
|
+
);
|
|
377
|
+
}
|
|
373
378
|
ensureAppDataDir();
|
|
374
379
|
const file = credentialsPath();
|
|
375
380
|
const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
|
|
@@ -1947,6 +1952,47 @@ var ALWAYS_ON_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
1947
1952
|
"enable_workflow_builder",
|
|
1948
1953
|
"health_check"
|
|
1949
1954
|
]);
|
|
1955
|
+
var FREE_TIER_EXTRA_READ_TOOLS = /* @__PURE__ */ new Set([
|
|
1956
|
+
"audit_workflows",
|
|
1957
|
+
// account-wide silent-failure audit — the free tier's wow moment
|
|
1958
|
+
"validate_workflow",
|
|
1959
|
+
// single-workflow pre-flight validation (read-only)
|
|
1960
|
+
"compare_locations",
|
|
1961
|
+
// cross-location config diff (reads only)
|
|
1962
|
+
"verify_funnel",
|
|
1963
|
+
// live funnel QA (reads + public URL probes)
|
|
1964
|
+
"check_blog_slug",
|
|
1965
|
+
// slug availability lookup
|
|
1966
|
+
"workflow_builder_status"
|
|
1967
|
+
// no-Firebase fallback diagnostic (pure status read)
|
|
1968
|
+
]);
|
|
1969
|
+
var FREE_TIER_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
|
|
1970
|
+
"export_account"
|
|
1971
|
+
// full-account export is a paid capability
|
|
1972
|
+
]);
|
|
1973
|
+
var FREE_TIER_EXCLUDED_MODULES = /* @__PURE__ */ new Set([
|
|
1974
|
+
"intake-to-build"
|
|
1975
|
+
// Blueprint lane is paid end-to-end
|
|
1976
|
+
]);
|
|
1977
|
+
var READ_PREFIXES = ["get_", "list_", "search_"];
|
|
1978
|
+
function isFreeTierTool(toolName, moduleName) {
|
|
1979
|
+
if (ALWAYS_ON_TOOL_NAMES.has(toolName)) return true;
|
|
1980
|
+
if (FREE_TIER_EXCLUDED_MODULES.has(moduleName.toLowerCase())) return false;
|
|
1981
|
+
if (FREE_TIER_EXCLUDED_TOOLS.has(toolName)) return false;
|
|
1982
|
+
if (FREE_TIER_EXTRA_READ_TOOLS.has(toolName)) return true;
|
|
1983
|
+
return READ_PREFIXES.some((p) => toolName.startsWith(p));
|
|
1984
|
+
}
|
|
1985
|
+
var FREE_TIER_DESC_SUFFIX = " [PAID TIER \u2014 this free license is read-only; calling this returns upgrade info, it will NOT perform the action.]";
|
|
1986
|
+
function freeTierUpgradeMessage(toolName) {
|
|
1987
|
+
return [
|
|
1988
|
+
`\`${toolName}\` needs the full GHL Command license \u2014 your free key is the read-only tier.`,
|
|
1989
|
+
"",
|
|
1990
|
+
"Free tier (what you have): every read \u2014 audits, account health, pipelines, contacts, conversations, funnels, workflows \u2014 on your own account.",
|
|
1991
|
+
"Full tier ($97/mo founding rate): Claude actually builds and fixes things \u2014 workflows, funnels, forms, pipelines, campaigns \u2014 plus the Workflow Builder, multi-location, and 3-machine activation.",
|
|
1992
|
+
"",
|
|
1993
|
+
"Upgrade at https://ghlcommand.com with the SAME email you used for the free key \u2014 your license upgrades in place (no reinstall, same key). Then fully restart Claude and this tool goes live."
|
|
1994
|
+
].join("\n");
|
|
1995
|
+
}
|
|
1950
1996
|
function parseList(raw) {
|
|
1951
1997
|
if (raw === void 0) return null;
|
|
1952
1998
|
const items = raw.split(/[,\s]+/).map((x) => x.trim().toLowerCase()).filter((x) => x.length > 0);
|
|
@@ -1974,7 +2020,7 @@ function shouldRegister(toolName, moduleName, config3) {
|
|
|
1974
2020
|
const toolEnabled = config3.enabledTools?.has(toolLower) ?? false;
|
|
1975
2021
|
return moduleEnabled || toolEnabled;
|
|
1976
2022
|
}
|
|
1977
|
-
function wrapServerForModule(realServer, moduleName, config3, attemptedTools, registeredTools) {
|
|
2023
|
+
function wrapServerForModule(realServer, moduleName, config3, attemptedTools, registeredTools, tier = "full", gatedTools) {
|
|
1978
2024
|
return new Proxy(realServer, {
|
|
1979
2025
|
get(target, prop, receiver) {
|
|
1980
2026
|
if (prop === "tool") {
|
|
@@ -1984,7 +2030,21 @@ function wrapServerForModule(realServer, moduleName, config3, attemptedTools, re
|
|
|
1984
2030
|
return void 0;
|
|
1985
2031
|
}
|
|
1986
2032
|
registeredTools.add(name);
|
|
1987
|
-
|
|
2033
|
+
let args = rest;
|
|
2034
|
+
if (tier === "free" && !isFreeTierTool(name, moduleName)) {
|
|
2035
|
+
gatedTools?.add(name);
|
|
2036
|
+
args = [...rest];
|
|
2037
|
+
const last = args.length - 1;
|
|
2038
|
+
if (last >= 0 && typeof args[last] === "function") {
|
|
2039
|
+
args[last] = async () => ({
|
|
2040
|
+
content: [{ type: "text", text: freeTierUpgradeMessage(name) }]
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
if (typeof args[0] === "string") {
|
|
2044
|
+
args[0] = args[0] + FREE_TIER_DESC_SUFFIX;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
return target.tool(name, ...args);
|
|
1988
2048
|
};
|
|
1989
2049
|
}
|
|
1990
2050
|
return Reflect.get(target, prop, receiver);
|
|
@@ -6115,22 +6175,85 @@ ${text2}`);
|
|
|
6115
6175
|
}
|
|
6116
6176
|
}
|
|
6117
6177
|
);
|
|
6178
|
+
function toCamel(meta) {
|
|
6179
|
+
return meta.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
6180
|
+
}
|
|
6181
|
+
function computeIntegrations(pageData) {
|
|
6182
|
+
const counts = {};
|
|
6183
|
+
let videoBackground = false;
|
|
6184
|
+
const sections = Array.isArray(pageData.sections) ? pageData.sections : [];
|
|
6185
|
+
for (const sec of sections) {
|
|
6186
|
+
const bg = sec.metaData?.extra?.bgImage?.value;
|
|
6187
|
+
if (bg && bg.mediaType === "video" && bg.videoUrl) videoBackground = true;
|
|
6188
|
+
const elements = Array.isArray(sec.elements) ? sec.elements : [];
|
|
6189
|
+
for (const el of elements) {
|
|
6190
|
+
if (el.type !== "element") continue;
|
|
6191
|
+
const meta = typeof el.meta === "string" ? el.meta : null;
|
|
6192
|
+
if (!meta) continue;
|
|
6193
|
+
const key = toCamel(meta);
|
|
6194
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
6195
|
+
}
|
|
6196
|
+
}
|
|
6197
|
+
const popupsList = pageData.popupsList;
|
|
6198
|
+
const popup = Array.isArray(popupsList) && popupsList.length > 0;
|
|
6199
|
+
return {
|
|
6200
|
+
videoBackground,
|
|
6201
|
+
blogMeta: { selectedBlogCategories: [], categoryNavigationList: [] },
|
|
6202
|
+
...counts,
|
|
6203
|
+
popup
|
|
6204
|
+
};
|
|
6205
|
+
}
|
|
6206
|
+
async function autosaveRequest(pageId, body) {
|
|
6207
|
+
const headers = await client.buildHeaders();
|
|
6208
|
+
headers.Origin = "https://page-builder.leadconnectorhq.com";
|
|
6209
|
+
headers.Referer = "https://page-builder.leadconnectorhq.com/";
|
|
6210
|
+
const url = `https://backend.leadconnectorhq.com/funnels/builder/autosave/${pageId}`;
|
|
6211
|
+
const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
|
|
6212
|
+
if (!response.ok) {
|
|
6213
|
+
throw new Error(`Funnel API Error ${response.status}: POST /builder/autosave/${pageId}
|
|
6214
|
+
${await response.text()}`);
|
|
6215
|
+
}
|
|
6216
|
+
const text = await response.text();
|
|
6217
|
+
if (!text) return {};
|
|
6218
|
+
try {
|
|
6219
|
+
return JSON.parse(text);
|
|
6220
|
+
} catch {
|
|
6221
|
+
return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6118
6224
|
server2.tool(
|
|
6119
6225
|
"update_page_content",
|
|
6120
|
-
"Update a page's builder content \u2014 sections, elements, settings, pageStyles, trackingCode, popups, popupsList, fontsForPreview. Use get_page_content first to see the current structure, modify it, then pass the full
|
|
6226
|
+
"Update a funnel/website page's builder content \u2014 sections, elements, settings, pageStyles, trackingCode, popups, popupsList, fontsForPreview. Use get_page_content first to see the current structure, modify it, then pass the full envelope here. Writes via GHL's builder autosave: the server uploads the page to storage, bumps the version, and regenerates the preview snapshot, then returns the new pageDataUrl. The funnelId and current version are resolved automatically from the page. Saves as a draft unless isPublished is true.",
|
|
6121
6227
|
{
|
|
6122
6228
|
pageId: import_zod34.z.string().describe("The page ID to update."),
|
|
6123
|
-
content: import_zod34.z.record(import_zod34.z.unknown()).describe("The full page content JSON
|
|
6124
|
-
|
|
6229
|
+
content: import_zod34.z.record(import_zod34.z.unknown()).describe("The full page content JSON \u2014 the 8-field envelope: sections, settings, general, pageStyles, trackingCode, fontsForPreview, popups, popupsList. Start from get_page_content and modify it."),
|
|
6230
|
+
funnelId: import_zod34.z.string().optional().describe("Funnel ID that owns the page. Optional \u2014 auto-resolved from the page metadata if omitted."),
|
|
6231
|
+
isPublished: import_zod34.z.boolean().optional().describe("Mark the page published after the save. Defaults to false (draft).")
|
|
6125
6232
|
},
|
|
6126
|
-
async ({ pageId, content, isPublished }) => {
|
|
6233
|
+
async ({ pageId, content, funnelId, isPublished }) => {
|
|
6127
6234
|
try {
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
pageId
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6235
|
+
let meta = {};
|
|
6236
|
+
try {
|
|
6237
|
+
meta = await funnelRequest("GET", `/page/${pageId}?locationId=${client.locationId}`);
|
|
6238
|
+
} catch (metaErr) {
|
|
6239
|
+
if (!funnelId) throw metaErr;
|
|
6240
|
+
}
|
|
6241
|
+
const resolvedFunnelId = funnelId ?? (typeof meta.funnelId === "string" ? meta.funnelId : void 0);
|
|
6242
|
+
if (!resolvedFunnelId) {
|
|
6243
|
+
throw new Error(`Could not resolve funnelId for page ${pageId}. Pass funnelId explicitly.`);
|
|
6244
|
+
}
|
|
6245
|
+
const pageVersion = typeof meta.pageVersion === "number" ? meta.pageVersion : typeof meta.version === "number" ? meta.version : 1;
|
|
6246
|
+
const pageData = {
|
|
6247
|
+
...content,
|
|
6248
|
+
pageVersion,
|
|
6249
|
+
pageType: isPublished ? "published" : "draft",
|
|
6250
|
+
manualSave: true,
|
|
6251
|
+
integrations: computeIntegrations(content)
|
|
6252
|
+
};
|
|
6253
|
+
const result = await autosaveRequest(pageId, {
|
|
6254
|
+
funnelId: resolvedFunnelId,
|
|
6255
|
+
pageVersion,
|
|
6256
|
+
pageData
|
|
6134
6257
|
});
|
|
6135
6258
|
return jsonResponse(result);
|
|
6136
6259
|
} catch (error) {
|
|
@@ -7146,6 +7269,7 @@ async function validateLicense(email, licenseKey) {
|
|
|
7146
7269
|
return {
|
|
7147
7270
|
ok: true,
|
|
7148
7271
|
installs: `${data.installs_used}/${data.installs_max}`,
|
|
7272
|
+
tier: data.tier === "free" ? "free" : data.tier === "full" ? "full" : void 0,
|
|
7149
7273
|
signedAttestation: typeof data.signed_attestation === "string" ? data.signed_attestation : void 0
|
|
7150
7274
|
};
|
|
7151
7275
|
}
|
|
@@ -7198,7 +7322,7 @@ async function validateFirebase(firebaseKey, refreshToken) {
|
|
|
7198
7322
|
function registerSetupTool(server2) {
|
|
7199
7323
|
server2.tool(
|
|
7200
7324
|
"setup_ghl_mcp",
|
|
7201
|
-
"First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all
|
|
7325
|
+
"First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 232 tools (180 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
|
|
7202
7326
|
{
|
|
7203
7327
|
email: import_zod39.z.string().email().describe("Email used at purchase."),
|
|
7204
7328
|
license_key: import_zod39.z.string().min(20).describe("License key from your purchase email."),
|
|
@@ -7279,24 +7403,27 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
|
|
|
7279
7403
|
// verified on every MCP startup. Closes the hand-crafted creds bypass.
|
|
7280
7404
|
signed_attestation: lic.signedAttestation
|
|
7281
7405
|
});
|
|
7282
|
-
const
|
|
7406
|
+
const isFree = lic.tier === "free";
|
|
7407
|
+
const toolCount = isFree ? workflowBuilderEnabled ? "106" : "88" : workflowBuilderEnabled ? "232" : "180";
|
|
7283
7408
|
const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
|
|
7284
|
-
const wfTip = workflowBuilderEnabled ? "" :
|
|
7409
|
+
const wfTip = workflowBuilderEnabled ? "" : isFree ? `
|
|
7410
|
+
Unlock the account auditor next (the free tier's best tool): say "Unlock the Workflow Builder" \u2014 one browser login enables audit_workflows, validate_workflow, and the full-detail reads.` : "\nTo enable Workflow Builder later (52 extra Firebase-gated tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
|
|
7411
|
+
const freeTip = isFree ? "\n\nFree tier: 106 read-only tools (88 before the one-login auditor unlock). Write tools stay visible but answer with upgrade info instead of acting. Full version ($97/mo founding rate, same key upgrades in place): https://ghlcommand.com" : "";
|
|
7285
7412
|
return {
|
|
7286
7413
|
content: [{
|
|
7287
7414
|
type: "text",
|
|
7288
7415
|
text: [
|
|
7289
7416
|
`Setup complete!`,
|
|
7290
7417
|
``,
|
|
7291
|
-
`License: verified (installs ${lic.installs}).`,
|
|
7418
|
+
`License: verified (installs ${lic.installs})${isFree ? " \u2014 FREE read-only tier" : ""}.`,
|
|
7292
7419
|
`GHL: connected to "${ghl.locationName}".`,
|
|
7293
7420
|
wfLine,
|
|
7294
7421
|
``,
|
|
7295
7422
|
`Credentials saved to: ${credentialsPath()}`,
|
|
7296
7423
|
``,
|
|
7297
|
-
`**Restart Claude (quit fully and reopen) to load all ${toolCount} tools.**`,
|
|
7424
|
+
`**Restart Claude (quit fully and reopen) to load all ${toolCount} ${isFree ? "read-only " : ""}tools.**`,
|
|
7298
7425
|
``,
|
|
7299
|
-
`After restart, try: "List my GHL contacts" or "Show my pipelines".${wfTip}`,
|
|
7426
|
+
`After restart, try: "List my GHL contacts" or "Show my pipelines".${wfTip}${freeTip}`,
|
|
7300
7427
|
workflowBuilderNote
|
|
7301
7428
|
].join("\n")
|
|
7302
7429
|
}]
|
|
@@ -7307,7 +7434,7 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
|
|
|
7307
7434
|
function registerEnableWorkflowBuilderTool(server2) {
|
|
7308
7435
|
server2.tool(
|
|
7309
7436
|
"enable_workflow_builder",
|
|
7310
|
-
"Add Firebase credentials to an existing GHL Command install to unlock 52 additional tools across the internal-API modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, and memberships, plus the pre-deploy validator. Requires you've already run setup_ghl_mcp. EASIEST PATH: run `capture_firebase_interactive` instead \u2014 a Chrome window opens, you log into GHL, zero pasting. Use THIS tool when you have JSON from `auto_capture_firebase_script` (console-paste path) to put in `firebase_paste`, or the three manual DevTools fields. Tool count goes from
|
|
7437
|
+
"Add Firebase credentials to an existing GHL Command install to unlock 52 additional tools across the internal-API modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, and memberships, plus the pre-deploy validator. On the FREE tier this same login unlocks the read-only auditor suite (audit_workflows, validate_workflow, full-detail workflow/funnel/pipeline reads). Requires you've already run setup_ghl_mcp. EASIEST PATH: run `capture_firebase_interactive` instead \u2014 a Chrome window opens, you log into GHL, zero pasting. Use THIS tool when you have JSON from `auto_capture_firebase_script` (console-paste path) to put in `firebase_paste`, or the three manual DevTools fields. Tool count goes from 180 to 232 after the next Claude restart.",
|
|
7311
7438
|
{
|
|
7312
7439
|
// v3.25.0: one-paste path. Tool runs `auto_capture_firebase_script` to
|
|
7313
7440
|
// get the console script; the script returns a JSON object that pastes
|
|
@@ -7388,7 +7515,7 @@ DevTools steps: https://elitedcs.com/ghl-mcp-firebase`
|
|
|
7388
7515
|
"",
|
|
7389
7516
|
"**You MUST restart Claude before using any workflow-builder tool.** Quit Claude completely (Cmd+Q on Mac, full exit on Windows) and reopen. Without a restart, the workflow builder tools will keep using the OLD Firebase auth from before this call and fail with 401 errors \u2014 even though this tool reported success.",
|
|
7390
7517
|
"",
|
|
7391
|
-
'After restart, all
|
|
7518
|
+
'After restart, all 232 tools load. Try: "List my workflows in full detail" or "Validate workflow <id>".',
|
|
7392
7519
|
"",
|
|
7393
7520
|
"Note: Firebase refresh tokens rotate every few weeks. If workflow tools stop working in a few weeks (run `health_check` to confirm Firebase auth: FAIL), run `auto_capture_firebase_script` for fresh values and re-run this tool with the new firebase_paste."
|
|
7394
7521
|
].join("\n")
|
|
@@ -7437,7 +7564,7 @@ function registerFirebaseCaptureScriptTool(server2) {
|
|
|
7437
7564
|
function registerInteractiveCaptureTool(server2) {
|
|
7438
7565
|
server2.tool(
|
|
7439
7566
|
"capture_firebase_interactive",
|
|
7440
|
-
"One-click Workflow Builder unlock (EASIEST PATH \u2014 try this before auto_capture_firebase_script). Opens a Chrome window where you just log into GHL; the MCP captures the Firebase credentials itself and saves them. No DevTools, no pasting. Re-run the same tool if the first call says it's still waiting for your login. After the first successful run, future re-captures (refresh-token rotations) happen silently with no window at all. Falls back cleanly: if no Chrome/Edge is installed, use auto_capture_firebase_script instead.",
|
|
7567
|
+
"One-click Workflow Builder unlock (EASIEST PATH \u2014 try this before auto_capture_firebase_script). Also the unlock for the FREE tier's account auditor (audit_workflows, validate_workflow) \u2014 same one-time login. Opens a Chrome window where you just log into GHL; the MCP captures the Firebase credentials itself and saves them. No DevTools, no pasting. Re-run the same tool if the first call says it's still waiting for your login. After the first successful run, future re-captures (refresh-token rotations) happen silently with no window at all. Falls back cleanly: if no Chrome/Edge is installed, use auto_capture_firebase_script instead.",
|
|
7441
7568
|
{},
|
|
7442
7569
|
async () => {
|
|
7443
7570
|
const existing = readCredentials();
|
|
@@ -7484,7 +7611,7 @@ The login in the capture window may belong to the wrong GHL account, or the sess
|
|
|
7484
7611
|
"",
|
|
7485
7612
|
"**You MUST restart Claude before using any workflow-builder tool.** Quit Claude completely (Cmd+Q on Mac, full exit on Windows) and reopen.",
|
|
7486
7613
|
"",
|
|
7487
|
-
'After restart, all
|
|
7614
|
+
'After restart, all 232 tools load. Try: "List my workflows in full detail".',
|
|
7488
7615
|
"",
|
|
7489
7616
|
"Future token rotations re-capture silently \u2014 if workflow tools ever 401, just run capture_firebase_interactive again; no window should appear."
|
|
7490
7617
|
].join("\n")
|
|
@@ -7568,7 +7695,7 @@ ${fallbackNote}`
|
|
|
7568
7695
|
function registerLeadCaptureTool(server2) {
|
|
7569
7696
|
server2.tool(
|
|
7570
7697
|
"request_license",
|
|
7571
|
-
"Get a GHL Command license. Use this if you installed from npm but don't have a license yet (or setup_ghl_mcp says your license is missing/invalid). GHL Command is $97/mo \u2014 every sub-account you manage, 3-machine activation, includes the only programmatic GHL workflow builder. Leave your email and we'll send the purchase link + setup help; the tool also returns where to buy right now.",
|
|
7698
|
+
"Get a GHL Command license. Use this if you installed from npm but don't have a license yet (or setup_ghl_mcp says your license is missing/invalid). GHL Command is $97/mo \u2014 every sub-account you manage, 3-machine activation, includes the only programmatic GHL workflow builder. There's also a FREE read-only tier (account audits + all reads on your own account) \u2014 instant key at https://ghlcommand.com/free. Leave your email and we'll send the purchase link + setup help; the tool also returns where to buy right now.",
|
|
7572
7699
|
{
|
|
7573
7700
|
email: import_zod39.z.string().email().describe("Your email \u2014 where to send the purchase link and setup help."),
|
|
7574
7701
|
name: import_zod39.z.string().optional().describe("Your name (optional).")
|
|
@@ -7592,6 +7719,7 @@ function registerLeadCaptureTool(server2) {
|
|
|
7592
7719
|
"GHL Command is $97/mo: every sub-account you manage, 3 machines, including the only programmatic GHL workflow builder. Cancel anytime.",
|
|
7593
7720
|
"",
|
|
7594
7721
|
`Buy now: ${url}`,
|
|
7722
|
+
"Want to try it first? FREE read-only tier (account audits + every read tool on your own account): https://ghlcommand.com/free",
|
|
7595
7723
|
"Your license key arrives by email within a couple minutes. Then run setup_ghl_mcp with your email + key."
|
|
7596
7724
|
].join("\n")
|
|
7597
7725
|
}]
|
|
@@ -8120,6 +8248,143 @@ ${lines.join("\n")}
|
|
|
8120
8248
|
}
|
|
8121
8249
|
}
|
|
8122
8250
|
);
|
|
8251
|
+
server2.tool(
|
|
8252
|
+
"create_sub_account",
|
|
8253
|
+
"Create a NEW GHL sub-account (location) under the agency, optionally loading a snapshot at creation \u2014 the first step of fully programmatic client provisioning (create \u2192 Blueprint build \u2192 verify_funnel). Requires an agency-level API key with the locations.write scope (add via register_agency_key). After creating, this probes whether the agency key can operate the new location and auto-registers it in the token registry when it can (switch_location then works immediately); if the probe fails, create a Private Integration Token inside the new sub-account and run register_location. Company Firebase (register_company_firebase) covers the internal-API builder tools in the new location either way.",
|
|
8254
|
+
{
|
|
8255
|
+
name: import_zod40.z.string().min(1).describe("Business / sub-account name."),
|
|
8256
|
+
snapshot_id: import_zod40.z.string().optional().describe("Snapshot ID to load at creation (see list_snapshots)."),
|
|
8257
|
+
company_id: import_zod40.z.string().optional().describe("Agency company ID. Defaults to the active location's registered companyId."),
|
|
8258
|
+
first_name: import_zod40.z.string().optional().describe("Prospect/owner first name."),
|
|
8259
|
+
last_name: import_zod40.z.string().optional().describe("Prospect/owner last name."),
|
|
8260
|
+
email: import_zod40.z.string().email().optional().describe("Prospect/owner email."),
|
|
8261
|
+
phone: import_zod40.z.string().optional().describe("Business phone (E.164, e.g. +15551234567)."),
|
|
8262
|
+
address: import_zod40.z.string().optional().describe("Street address."),
|
|
8263
|
+
city: import_zod40.z.string().optional(),
|
|
8264
|
+
state: import_zod40.z.string().optional(),
|
|
8265
|
+
postal_code: import_zod40.z.string().optional(),
|
|
8266
|
+
country: import_zod40.z.string().optional().describe("Two-letter country code. Defaults to US."),
|
|
8267
|
+
website: import_zod40.z.string().optional(),
|
|
8268
|
+
timezone: import_zod40.z.string().optional().describe("IANA timezone, e.g. America/Los_Angeles."),
|
|
8269
|
+
auto_register: import_zod40.z.boolean().optional().describe("Probe the new location with the agency key and add it to the token registry on success. Default true.")
|
|
8270
|
+
},
|
|
8271
|
+
async (args) => {
|
|
8272
|
+
const agencyKey = registry2?.getAgencyKey();
|
|
8273
|
+
if (!agencyKey) {
|
|
8274
|
+
return {
|
|
8275
|
+
content: [{ type: "text", text: "No agency key registered. Run register_agency_key with an agency-level Private Integration key (it needs the locations.write scope to create sub-accounts)." }],
|
|
8276
|
+
isError: true
|
|
8277
|
+
};
|
|
8278
|
+
}
|
|
8279
|
+
const companyId = args.company_id || (client.defaultLocationId ? registry2?.getToken(client.defaultLocationId)?.companyId : void 0) || process.env.GHL_COMPANY_ID;
|
|
8280
|
+
if (!companyId) {
|
|
8281
|
+
return {
|
|
8282
|
+
content: [{ type: "text", text: "Couldn't resolve the agency company ID \u2014 pass company_id explicitly (the GHL agency/company ID, not a location ID)." }],
|
|
8283
|
+
isError: true
|
|
8284
|
+
};
|
|
8285
|
+
}
|
|
8286
|
+
try {
|
|
8287
|
+
const agencyClient = new GHLClient({ apiKey: agencyKey });
|
|
8288
|
+
const prospect = args.first_name || args.last_name || args.email ? { prospectInfo: { ...args.first_name && { firstName: args.first_name }, ...args.last_name && { lastName: args.last_name }, ...args.email && { email: args.email } } } : {};
|
|
8289
|
+
const body = {
|
|
8290
|
+
name: args.name,
|
|
8291
|
+
companyId,
|
|
8292
|
+
country: args.country || "US",
|
|
8293
|
+
...args.snapshot_id && { snapshotId: args.snapshot_id },
|
|
8294
|
+
...args.phone && { phone: args.phone },
|
|
8295
|
+
...args.address && { address: args.address },
|
|
8296
|
+
...args.city && { city: args.city },
|
|
8297
|
+
...args.state && { state: args.state },
|
|
8298
|
+
...args.postal_code && { postalCode: args.postal_code },
|
|
8299
|
+
...args.website && { website: args.website },
|
|
8300
|
+
...args.timezone && { timezone: args.timezone },
|
|
8301
|
+
...prospect
|
|
8302
|
+
};
|
|
8303
|
+
const created = await agencyClient.post("/locations/", { body, noRetry: true });
|
|
8304
|
+
const loc = created.location ?? created;
|
|
8305
|
+
const locationId2 = loc.id || loc._id;
|
|
8306
|
+
if (!locationId2) {
|
|
8307
|
+
return {
|
|
8308
|
+
content: [{ type: "text", text: `Sub-account create returned no location id. Raw response:
|
|
8309
|
+
${JSON.stringify(created, null, 2)}` }],
|
|
8310
|
+
isError: true
|
|
8311
|
+
};
|
|
8312
|
+
}
|
|
8313
|
+
let probeOk = false;
|
|
8314
|
+
let probeNote = "";
|
|
8315
|
+
try {
|
|
8316
|
+
await agencyClient.get("/contacts/", { params: { locationId: locationId2, limit: 1 } });
|
|
8317
|
+
probeOk = true;
|
|
8318
|
+
} catch (e) {
|
|
8319
|
+
probeNote = e instanceof Error ? e.message : String(e);
|
|
8320
|
+
}
|
|
8321
|
+
let registered = false;
|
|
8322
|
+
if (probeOk && args.auto_register !== false && registry2) {
|
|
8323
|
+
registry2.registerLocation(locationId2, args.name, agencyKey, companyId);
|
|
8324
|
+
registered = true;
|
|
8325
|
+
}
|
|
8326
|
+
return {
|
|
8327
|
+
content: [{
|
|
8328
|
+
type: "text",
|
|
8329
|
+
text: [
|
|
8330
|
+
`Sub-account created: "${args.name}"`,
|
|
8331
|
+
`Location ID: ${locationId2}`,
|
|
8332
|
+
args.snapshot_id ? `Snapshot ${args.snapshot_id} is loading (GHL applies snapshots asynchronously \u2014 assets appear over the next minute or two).` : "No snapshot loaded.",
|
|
8333
|
+
probeOk ? registered ? "Agency key CAN operate this location \u2014 added to the token registry. Run switch_location to start building in it." : "Agency key CAN operate this location (auto_register was off \u2014 run register_location to add it)." : `Agency key can NOT read this location's data (${probeNote.slice(0, 120)}). Create a Private Integration Token inside the new sub-account, then run register_location. The company Firebase still covers the workflow/funnel/form builder tools there.`
|
|
8334
|
+
].join("\n")
|
|
8335
|
+
}]
|
|
8336
|
+
};
|
|
8337
|
+
} catch (error) {
|
|
8338
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8339
|
+
const scopeHint = /401|403|scope|unauthorized/i.test(message) ? "\n\nThis usually means the registered agency key lacks the locations.write scope. Edit the agency Private Integration in GHL (Agency Settings \u2192 Private Integrations) and enable it." : "";
|
|
8340
|
+
return {
|
|
8341
|
+
content: [{ type: "text", text: `Error creating sub-account: ${message}${scopeHint}` }],
|
|
8342
|
+
isError: true
|
|
8343
|
+
};
|
|
8344
|
+
}
|
|
8345
|
+
}
|
|
8346
|
+
);
|
|
8347
|
+
server2.tool(
|
|
8348
|
+
"delete_sub_account",
|
|
8349
|
+
"Permanently delete a GHL sub-account (location) via the agency API. IRREVERSIBLE \u2014 every contact, workflow, funnel, and setting inside the sub-account is destroyed. Refuses to delete the currently active location. Also removes the location from the token registry. Requires an agency-level key with locations.write.",
|
|
8350
|
+
{
|
|
8351
|
+
location_id: import_zod40.z.string().min(1).describe("The location ID to delete."),
|
|
8352
|
+
confirm: import_zod40.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
|
|
8353
|
+
delete_twilio: import_zod40.z.boolean().optional().describe("Also delete the location's Twilio sub-account. Default false.")
|
|
8354
|
+
},
|
|
8355
|
+
async ({ location_id, delete_twilio }) => {
|
|
8356
|
+
const agencyKey = registry2?.getAgencyKey();
|
|
8357
|
+
if (!agencyKey) {
|
|
8358
|
+
return {
|
|
8359
|
+
content: [{ type: "text", text: "No agency key registered. Run register_agency_key first." }],
|
|
8360
|
+
isError: true
|
|
8361
|
+
};
|
|
8362
|
+
}
|
|
8363
|
+
if (client.defaultLocationId === location_id) {
|
|
8364
|
+
return {
|
|
8365
|
+
content: [{ type: "text", text: `Refusing to delete the currently ACTIVE location (${location_id}). switch_location to a different sub-account first.` }],
|
|
8366
|
+
isError: true
|
|
8367
|
+
};
|
|
8368
|
+
}
|
|
8369
|
+
try {
|
|
8370
|
+
const agencyClient = new GHLClient({ apiKey: agencyKey });
|
|
8371
|
+
await agencyClient.delete(`/locations/${location_id}`, { params: { deleteTwilioAccount: delete_twilio ?? false } });
|
|
8372
|
+
const wasRegistered = registry2?.removeLocation(location_id) ?? false;
|
|
8373
|
+
return {
|
|
8374
|
+
content: [{
|
|
8375
|
+
type: "text",
|
|
8376
|
+
text: `Sub-account ${location_id} permanently deleted.${wasRegistered ? " Removed from the token registry." : ""}`
|
|
8377
|
+
}]
|
|
8378
|
+
};
|
|
8379
|
+
} catch (error) {
|
|
8380
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8381
|
+
return {
|
|
8382
|
+
content: [{ type: "text", text: `Error deleting sub-account: ${message}` }],
|
|
8383
|
+
isError: true
|
|
8384
|
+
};
|
|
8385
|
+
}
|
|
8386
|
+
}
|
|
8387
|
+
);
|
|
8123
8388
|
}
|
|
8124
8389
|
|
|
8125
8390
|
// src/tools/bulk-operations.ts
|
|
@@ -13986,11 +14251,12 @@ var KNOWN_MODULES = /* @__PURE__ */ new Set([
|
|
|
13986
14251
|
LOCATION_SWITCHER_MODULE,
|
|
13987
14252
|
SNAPSHOTS_MODULE
|
|
13988
14253
|
]);
|
|
13989
|
-
function registerAllTools(server2, client, registry2, mcpVersion, env = process.env) {
|
|
14254
|
+
function registerAllTools(server2, client, registry2, mcpVersion, env = process.env, tier = "full") {
|
|
13990
14255
|
const config3 = parseAllowlist(env);
|
|
13991
14256
|
const attemptedTools = /* @__PURE__ */ new Set();
|
|
13992
14257
|
const registeredTools = /* @__PURE__ */ new Set();
|
|
13993
|
-
const
|
|
14258
|
+
const gatedTools = /* @__PURE__ */ new Set();
|
|
14259
|
+
const wrap = (moduleName) => wrapServerForModule(server2, moduleName, config3, attemptedTools, registeredTools, tier, gatedTools);
|
|
13994
14260
|
for (const [register, moduleName] of publicApiTools) {
|
|
13995
14261
|
register(wrap(moduleName), client);
|
|
13996
14262
|
}
|
|
@@ -14020,6 +14286,13 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
|
|
|
14020
14286
|
if (isAllowlistActive(config3)) {
|
|
14021
14287
|
validateAndLog(config3, KNOWN_MODULES, attemptedTools, registeredTools);
|
|
14022
14288
|
}
|
|
14289
|
+
if (tier === "free") {
|
|
14290
|
+
process.stderr.write(
|
|
14291
|
+
`[ghl-mcp] FREE tier: ${registeredTools.size - gatedTools.size} read-only tools live; ${gatedTools.size} write tools answer with upgrade info. Full version: https://ghlcommand.com
|
|
14292
|
+
`
|
|
14293
|
+
);
|
|
14294
|
+
}
|
|
14295
|
+
return { registeredTools, gatedTools };
|
|
14023
14296
|
}
|
|
14024
14297
|
|
|
14025
14298
|
// src/attestation.ts
|
|
@@ -14027,6 +14300,9 @@ var import_node_crypto = require("node:crypto");
|
|
|
14027
14300
|
var ATTESTATION_PUBLIC_KEY_B64 = "8KJo8V3Z7ngeToIQfOXpKdlr/EA34hXJVEIpW0A7wqs=";
|
|
14028
14301
|
var ATTESTATION_VERSION = 1;
|
|
14029
14302
|
var ATTESTATION_GRACE_DAYS = 14;
|
|
14303
|
+
function tierFromPayload(payload) {
|
|
14304
|
+
return payload?.tier === "free" ? "free" : "full";
|
|
14305
|
+
}
|
|
14030
14306
|
function deviceFingerprint2() {
|
|
14031
14307
|
const os3 = require("node:os");
|
|
14032
14308
|
const crypto4 = require("node:crypto");
|
|
@@ -14051,6 +14327,20 @@ async function getPublicKey() {
|
|
|
14051
14327
|
);
|
|
14052
14328
|
return cachedPublicKey;
|
|
14053
14329
|
}
|
|
14330
|
+
function parseAttestationPayload(token) {
|
|
14331
|
+
try {
|
|
14332
|
+
const dot = token.indexOf(".");
|
|
14333
|
+
if (dot <= 0 || dot === token.length - 1) return null;
|
|
14334
|
+
const payloadJson = new TextDecoder().decode(base64urlDecode(token.slice(0, dot)));
|
|
14335
|
+
const payload = JSON.parse(payloadJson);
|
|
14336
|
+
if (typeof payload.v !== "number" || typeof payload.email !== "string" || typeof payload.license_key !== "string" || typeof payload.device_fingerprint !== "string" || typeof payload.installs_max !== "number" || typeof payload.issued_at !== "string" || typeof payload.expires_at !== "string") {
|
|
14337
|
+
return null;
|
|
14338
|
+
}
|
|
14339
|
+
return payload;
|
|
14340
|
+
} catch {
|
|
14341
|
+
return null;
|
|
14342
|
+
}
|
|
14343
|
+
}
|
|
14054
14344
|
async function verifyAttestation(token, bindings, now = /* @__PURE__ */ new Date()) {
|
|
14055
14345
|
if (typeof token !== "string" || !token) return { ok: false, reason: "malformed" };
|
|
14056
14346
|
const dot = token.indexOf(".");
|
|
@@ -14538,6 +14828,8 @@ function renewAttestationInBackground(creds) {
|
|
|
14538
14828
|
async function resolveAccessAndRegister() {
|
|
14539
14829
|
let licenseVerified = false;
|
|
14540
14830
|
let attestationStatus = "no-creds";
|
|
14831
|
+
let sessionTier = "full";
|
|
14832
|
+
const tierFromDisk = () => tierFromPayload(parseAttestationPayload(readCredentials()?.signed_attestation ?? ""));
|
|
14541
14833
|
if (fileCreds?.email && fileCreds?.license_key && fileCreds.signed_attestation) {
|
|
14542
14834
|
const verify = await verifyAttestation(fileCreds.signed_attestation, {
|
|
14543
14835
|
email: fileCreds.email,
|
|
@@ -14546,6 +14838,7 @@ async function resolveAccessAndRegister() {
|
|
|
14546
14838
|
if (verify.ok) {
|
|
14547
14839
|
licenseVerified = true;
|
|
14548
14840
|
attestationStatus = verify.reason;
|
|
14841
|
+
sessionTier = tierFromPayload(verify.payload);
|
|
14549
14842
|
void renewAttestationInBackground(fileCreds);
|
|
14550
14843
|
} else {
|
|
14551
14844
|
process.stderr.write(`[ghl-mcp] Stored attestation rejected: ${verify.reason}. Re-validating online.
|
|
@@ -14554,6 +14847,7 @@ async function resolveAccessAndRegister() {
|
|
|
14554
14847
|
if (renewed === "renewed") {
|
|
14555
14848
|
licenseVerified = true;
|
|
14556
14849
|
attestationStatus = "renewed-after-tamper";
|
|
14850
|
+
sessionTier = tierFromDisk();
|
|
14557
14851
|
} else if (renewed === "renewed-unsigned" && Date.now() < TRANSITIONAL_ATTESTATION_DEADLINE) {
|
|
14558
14852
|
licenseVerified = true;
|
|
14559
14853
|
attestationStatus = "renewed-after-tamper";
|
|
@@ -14567,6 +14861,7 @@ async function resolveAccessAndRegister() {
|
|
|
14567
14861
|
if (renewed === "renewed") {
|
|
14568
14862
|
licenseVerified = true;
|
|
14569
14863
|
attestationStatus = "renewed-from-legacy";
|
|
14864
|
+
sessionTier = tierFromDisk();
|
|
14570
14865
|
} else if (renewed === "renewed-unsigned" && Date.now() < TRANSITIONAL_ATTESTATION_DEADLINE) {
|
|
14571
14866
|
licenseVerified = true;
|
|
14572
14867
|
attestationStatus = "renewed-from-legacy";
|
|
@@ -14591,6 +14886,7 @@ async function resolveAccessAndRegister() {
|
|
|
14591
14886
|
if (lic.ok && acceptUnsigned) {
|
|
14592
14887
|
licenseVerified = true;
|
|
14593
14888
|
attestationStatus = "renewed";
|
|
14889
|
+
sessionTier = lic.signedAttestation ? tierFromPayload(parseAttestationPayload(lic.signedAttestation)) : lic.tier === "free" ? "free" : "full";
|
|
14594
14890
|
try {
|
|
14595
14891
|
writeCredentials({
|
|
14596
14892
|
license_key: licKey,
|
|
@@ -14618,7 +14914,7 @@ async function resolveAccessAndRegister() {
|
|
|
14618
14914
|
}
|
|
14619
14915
|
}
|
|
14620
14916
|
if (licenseVerified) {
|
|
14621
|
-
process.stderr.write(`[ghl-mcp] License gate: ${attestationStatus}.
|
|
14917
|
+
process.stderr.write(`[ghl-mcp] License gate: ${attestationStatus}${sessionTier === "free" ? " (FREE read-only tier)" : ""}.
|
|
14622
14918
|
`);
|
|
14623
14919
|
}
|
|
14624
14920
|
inBootstrapMode = !apiKey || !locationId || !licenseVerified;
|
|
@@ -14634,7 +14930,7 @@ async function resolveAccessAndRegister() {
|
|
|
14634
14930
|
registerFirebaseCaptureScriptTool(server);
|
|
14635
14931
|
} else {
|
|
14636
14932
|
const client = new GHLClient({ apiKey, locationId });
|
|
14637
|
-
registerAllTools(server, client, registry, pkg.version);
|
|
14933
|
+
registerAllTools(server, client, registry, pkg.version, process.env, sessionTier);
|
|
14638
14934
|
registerEnableWorkflowBuilderTool(server);
|
|
14639
14935
|
registerFirebaseCaptureScriptTool(server);
|
|
14640
14936
|
registerInteractiveCaptureTool(server);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.52.0",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
|
-
"description": "GoHighLevel MCP Server for Claude.
|
|
5
|
+
"description": "GoHighLevel MCP Server for Claude. 232 tools — 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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ghl-mcp": "dist/index.js"
|