@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 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.** 229 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.
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 (229 tools total with the optional Workflow Builder Firebase add-on).
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 177 tools work fine — you just won't have workflow/funnel/form/pipeline editing. This is the flagship feature: do this step.
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 229 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`:
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.49.1",
34
+ version: "3.52.0",
35
35
  mcpName: "io.github.drjerryrelth/ghl-command",
36
- description: "GoHighLevel MCP Server for Claude. 229 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.",
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
- return target.tool(name, ...rest);
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 8-field envelope here. GHL's UI fires three POSTs to this endpoint per save (one collaborative-edit broadcast + two commits); for MCP use, one POST with write=true is enough.",
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. Expected fields: sections, settings, general, pageStyles, trackingCode, popups, popupsList, fontsForPreview."),
6124
- isPublished: import_zod34.z.boolean().optional().describe("Whether the page should be marked published after the save. Defaults to false (draft).")
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
- const result = await funnelRequest("POST", `/builder/prebuilt-section/sync/changes`, {
6129
- locationId: client.locationId,
6130
- pageId,
6131
- pageData: content,
6132
- write: true,
6133
- isPublished: isPublished ?? false
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 229 tools (177 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
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 toolCount = workflowBuilderEnabled ? "229" : "177";
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 ? "" : "\nTo enable Workflow Builder later (49 extra Firebase-gated tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
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 177 to 229 after the next Claude restart.",
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 229 tools load. Try: "List my workflows in full detail" or "Validate workflow <id>".',
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 229 tools load. Try: "List my workflows in full detail".',
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 wrap = (moduleName) => wrapServerForModule(server2, moduleName, config3, attemptedTools, registeredTools);
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.49.1",
3
+ "version": "3.52.0",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
- "description": "GoHighLevel MCP Server for Claude. 229 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.",
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"