@elitedcs/ghl-mcp 3.48.2 → 3.49.1

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,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.49.1 — Docs patch: one-click capture is now the primary Milestone-2 path
4
+
5
+ No behavior changes; text only.
6
+
7
+ - README "Enable the Workflow Builder": `capture_firebase_interactive` (log into a
8
+ Chrome window, zero pasting) is the supported path; the console-paste script is
9
+ demoted to a collapsed fallback (still the path for locked-down machines and for
10
+ capturing CLIENT accounts in multi-tenant). Rotation note now points at the
11
+ silent re-capture.
12
+ - `enable_workflow_builder` and `auto_capture_firebase_script` tool descriptions
13
+ point to `capture_firebase_interactive` first.
14
+
15
+
16
+ ## 3.49.0 — One-click Workflow Builder unlock (`capture_firebase_interactive`)
17
+
18
+ The buyer's whole job is now: log into GHL in a Chrome window the tool opens.
19
+ No DevTools, no console paste, no JSON shuttling.
20
+
21
+ - **New tool `capture_firebase_interactive`** (normal mode). Spawns a detached
22
+ helper (`dist/capture-helper.js`) that launches the buyer's own installed
23
+ Chrome (playwright-core channel discovery — Edge fallback, zero browser
24
+ downloads) into a dedicated persistent profile, waits for a GHL login, reads
25
+ the three Firebase values straight out of IndexedDB (same selection logic as
26
+ the console script), validates them against Firebase, and saves them via the
27
+ same path as `enable_workflow_builder`.
28
+ - **Silent re-capture for token rotations.** The persistent profile keeps the
29
+ GHL session cookie, so every capture after the first tries HEADLESS first —
30
+ refresh-token rotation becomes "run the tool again, no window appears".
31
+ - **Timeout-proof by design.** The helper is fully detached and communicates
32
+ through a state file; the tool call polls with a generous window and tells
33
+ the buyer to re-run if they're still mid-login. An MCP client timeout can
34
+ never strand the flow.
35
+ - **Fallbacks intact.** No Chrome/Edge, closed window, or login timeout all
36
+ return clear instructions pointing at `auto_capture_firebase_script` (the
37
+ console-paste path, unchanged).
38
+ - Tool count: 228 → 229 (177 without Firebase).
39
+
40
+
3
41
  ## 3.48.2 — Docs patch: tool-count claims corrected (the real number is 228)
4
42
 
5
43
  No behavior changes; text only, plus one new test.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GHL Command — GoHighLevel MCP Server
2
2
 
3
- **Full GoHighLevel API access for Claude.** 228 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.** 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.
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
 
@@ -116,7 +116,7 @@ Run setup_ghl_mcp to activate GHL Command:
116
116
  ghl_location_id: YOUR_LOCATION_ID
117
117
  ```
118
118
 
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 (228 tools total with the optional Workflow Builder Firebase add-on).
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).
120
120
 
121
121
  ### 4. Try it
122
122
 
@@ -156,9 +156,18 @@ https://app.gohighlevel.com/v2/location/YOUR_LOCATION_ID/dashboard
156
156
 
157
157
  ## Enable the Workflow Builder
158
158
 
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 176 tools work fine — you just won't have workflow/funnel/form/pipeline editing. This is the flagship feature: do this step.
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.
160
160
 
161
- **One-paste capture (the supported path):**
161
+ **One-click capture (v3.49.0+, the supported path):**
162
+
163
+ 1. In Claude, ask: `Run capture_firebase_interactive`. Your installed Chrome opens a dedicated window on the GHL login page (no browser download; it never touches your daily browsing profile).
164
+ 2. Log into your GHL account in that window. That's the whole job — the tool reads the three Firebase values itself, validates them against Firebase, and saves them. If the call says it's still waiting, finish logging in and run the same tool again.
165
+ 3. Restart Claude. The Firebase-gated tools are live.
166
+
167
+ > Firebase refresh tokens can rotate. If workflow tools stop working after a few weeks (`health_check` shows Firebase auth: FAIL), just re-run `capture_firebase_interactive` — after your first login it re-captures silently, no window at all.
168
+
169
+ <details>
170
+ <summary><strong>Console-paste fallback</strong> (no Chrome/Edge installed, the window path fails, or you're capturing a CLIENT account for multi-tenant)</summary>
162
171
 
163
172
  1. In Claude, ask: `Run auto_capture_firebase_script`. It returns a small browser-console script.
164
173
  2. In Chrome, open a tab logged into your GHL account. Press **F12** (Windows) or **Cmd+Option+I** (Mac), click the **Console** tab, paste the script, press Enter. If Chrome blocks the paste, type `allow pasting` in the Console first, then paste again.
@@ -168,7 +177,7 @@ The builder + cloner + validator tools (workflow builder, funnel builder, form b
168
177
 
169
178
  Screenshots and the same script behind a copy button: [elitedcs.com/ghl-mcp-firebase](https://elitedcs.com/ghl-mcp-firebase)
170
179
 
171
- > Firebase refresh tokens can rotate. If workflow tools stop working after a few weeks (`health_check` shows Firebase auth: FAIL), re-run `auto_capture_firebase_script` for fresh values and pass them to `enable_workflow_builder` again.
180
+ </details>
172
181
 
173
182
  <details>
174
183
  <summary><strong>Manual fallback</strong> (locked-down browsers that block console pasting or clipboard access)</summary>
@@ -736,7 +745,7 @@ Source repo is private. Contributors need an invitation from `drjerryrelth`. The
736
745
 
737
746
  ### Reducing context / token usage
738
747
 
739
- Every registered MCP tool's schema is shipped to the model on every message. With 228 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`:
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`:
740
749
 
741
750
  ```jsonc
742
751
  // Claude Desktop config — enable whole modules
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/interactive-capture.ts
26
+ var fs = __toESM(require("fs"));
27
+ var path2 = __toESM(require("path"));
28
+ var import_zod2 = require("zod");
29
+
30
+ // src/credentials-store.ts
31
+ var path = __toESM(require("path"));
32
+ var os = __toESM(require("os"));
33
+ var import_zod = require("zod");
34
+ var APP_NAME = "elitedcs-ghl-mcp";
35
+ var CredentialsSchema = import_zod.z.object({
36
+ license_key: import_zod.z.string().min(1),
37
+ email: import_zod.z.string().email(),
38
+ verified_at: import_zod.z.string().min(1),
39
+ ghl_api_key: import_zod.z.string().min(1),
40
+ ghl_location_id: import_zod.z.string().min(1),
41
+ ghl_company_id: import_zod.z.string().optional(),
42
+ ghl_user_id: import_zod.z.string().optional(),
43
+ ghl_firebase_api_key: import_zod.z.string().optional(),
44
+ ghl_firebase_refresh_token: import_zod.z.string().optional(),
45
+ // Ed25519-signed attestation from elitedcs.com/api/validate-license.
46
+ // v3.24.0+ writes this on every successful online validation; index.ts
47
+ // verifies on startup and refuses to load tools without it (closing the
48
+ // hand-crafted-credentials.json bypass that existed through v3.23.0).
49
+ // Optional in the schema only so files written by earlier versions still
50
+ // parse — index.ts's gate forces a re-validate when the field is missing.
51
+ signed_attestation: import_zod.z.string().optional()
52
+ });
53
+ function appDataDir() {
54
+ const override = process.env.GHL_MCP_CONFIG_DIR?.trim();
55
+ if (override) {
56
+ if (!path.isAbsolute(override)) {
57
+ throw new Error(
58
+ `GHL_MCP_CONFIG_DIR must be an absolute path (got "${override}"). Use e.g. /data/ghl-mcp in a container, with a volume mounted at /data.`
59
+ );
60
+ }
61
+ return override;
62
+ }
63
+ const home = os.homedir();
64
+ if (process.platform === "darwin") {
65
+ return path.join(home, "Library", "Application Support", APP_NAME);
66
+ }
67
+ if (process.platform === "win32") {
68
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
69
+ return path.join(appData, APP_NAME);
70
+ }
71
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
72
+ return path.join(xdg, APP_NAME);
73
+ }
74
+
75
+ // src/interactive-capture.ts
76
+ function chromeProfileDir() {
77
+ return path2.join(appDataDir(), "chrome-profile");
78
+ }
79
+ function captureStatePath() {
80
+ return path2.join(appDataDir(), "interactive-capture.json");
81
+ }
82
+ var CaptureStateSchema = import_zod2.z.object({
83
+ status: import_zod2.z.enum(["waiting_login", "captured", "timeout", "error"]),
84
+ startedAt: import_zod2.z.string(),
85
+ updatedAt: import_zod2.z.string(),
86
+ // Present when status === "captured"
87
+ ghl_firebase_api_key: import_zod2.z.string().optional(),
88
+ ghl_user_id: import_zod2.z.string().optional(),
89
+ ghl_firebase_refresh_token: import_zod2.z.string().optional(),
90
+ account_email: import_zod2.z.string().optional(),
91
+ // Present when status === "error"
92
+ error: import_zod2.z.string().optional()
93
+ });
94
+ function writeCaptureState(state) {
95
+ const dir = appDataDir();
96
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
97
+ fs.writeFileSync(captureStatePath(), JSON.stringify(state, null, 2), {
98
+ mode: 384
99
+ });
100
+ }
101
+ var EXTRACT_FIREBASE_PAGE_FN = `async () => {
102
+ const pick = (rows) => {
103
+ const candidates = rows.filter(r => typeof r?.fbase_key === 'string' && r.fbase_key.startsWith('firebase:authUser:AIza') && r?.value?.apiKey && r?.value?.uid && r?.value?.stsTokenManager?.refreshToken);
104
+ if (!candidates.length) return null;
105
+ const v = candidates[0].value;
106
+ return {
107
+ ghl_firebase_api_key: v.apiKey,
108
+ ghl_user_id: v.uid,
109
+ ghl_firebase_refresh_token: v.stsTokenManager.refreshToken,
110
+ account_email: v.email || null,
111
+ };
112
+ };
113
+ try {
114
+ let poisoned = false;
115
+ if (typeof indexedDB.databases === 'function') {
116
+ const dbs = await indexedDB.databases();
117
+ if (dbs.some(d => d.name === 'firebaseLocalStorageDb')) {
118
+ const db = await new Promise((res, rej) => {
119
+ const r = indexedDB.open('firebaseLocalStorageDb');
120
+ r.onsuccess = () => res(r.result);
121
+ r.onerror = () => rej(r.error);
122
+ });
123
+ if (db.objectStoreNames.contains('firebaseLocalStorage')) {
124
+ const tx = db.transaction('firebaseLocalStorage', 'readonly');
125
+ const store = tx.objectStore('firebaseLocalStorage');
126
+ const rows = await new Promise((res, rej) => {
127
+ const r = store.getAll();
128
+ r.onsuccess = () => res(r.result);
129
+ r.onerror = () => rej(r.error);
130
+ });
131
+ db.close();
132
+ const found = pick(rows);
133
+ if (found) return { found };
134
+ } else {
135
+ db.close();
136
+ poisoned = true;
137
+ }
138
+ }
139
+ }
140
+ const lsRows = [];
141
+ for (let i = 0; i < localStorage.length; i++) {
142
+ const k = localStorage.key(i);
143
+ if (k && k.startsWith('firebase:authUser:AIza')) {
144
+ try { lsRows.push({ fbase_key: k, value: JSON.parse(localStorage.getItem(k)) }); } catch (e) {}
145
+ }
146
+ }
147
+ const fromLs = pick(lsRows);
148
+ if (fromLs) return { found: fromLs };
149
+ return poisoned ? { poisoned: true } : null;
150
+ } catch (e) {
151
+ return null;
152
+ }
153
+ }`;
154
+ var HEAL_POISONED_DB_PAGE_FN = `() => new Promise((resolve) => {
155
+ const r = indexedDB.deleteDatabase('firebaseLocalStorageDb');
156
+ const t = setTimeout(() => resolve('timeout'), 5000);
157
+ r.onsuccess = () => { clearTimeout(t); resolve('deleted'); };
158
+ r.onblocked = () => { clearTimeout(t); resolve('blocked'); };
159
+ r.onerror = () => { clearTimeout(t); resolve('error'); };
160
+ })`;
161
+ var EXTRACT_FIREBASE_EXPRESSION = `(${EXTRACT_FIREBASE_PAGE_FN})()`;
162
+ var HEAL_POISONED_DB_EXPRESSION = `(${HEAL_POISONED_DB_PAGE_FN})()`;
163
+ var GHL_APP_URL = "https://app.gohighlevel.com/";
164
+ var HELPER_LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
165
+ var HEADLESS_PROBE_TIMEOUT_MS = 30 * 1e3;
166
+ function profileExists() {
167
+ return fs.existsSync(chromeProfileDir());
168
+ }
169
+ var CAPTURED_FRESH_MS = 10 * 60 * 1e3;
170
+ var WAITING_FRESH_MS = HELPER_LOGIN_TIMEOUT_MS + 30 * 1e3;
171
+
172
+ // src/capture-helper.ts
173
+ var now = () => (/* @__PURE__ */ new Date()).toISOString();
174
+ function writeState(partial, startedAt) {
175
+ writeCaptureState({ ...partial, startedAt, updatedAt: now() });
176
+ }
177
+ async function launchContext(headless) {
178
+ const { chromium } = await import("playwright-core");
179
+ const channels = ["chrome", "msedge"];
180
+ let lastError;
181
+ for (const channel of channels) {
182
+ try {
183
+ return await chromium.launchPersistentContext(chromeProfileDir(), {
184
+ channel,
185
+ headless,
186
+ viewport: null
187
+ });
188
+ } catch (e) {
189
+ lastError = e;
190
+ }
191
+ }
192
+ throw lastError instanceof Error ? lastError : new Error("No Chrome or Edge installation found");
193
+ }
194
+ async function pollPage(context, timeoutMs) {
195
+ const page = context.pages()[0] ?? await context.newPage();
196
+ await page.goto(GHL_APP_URL, { waitUntil: "domcontentloaded", timeout: 6e4 }).catch(() => {
197
+ });
198
+ const deadline = Date.now() + timeoutMs;
199
+ let healed = false;
200
+ while (Date.now() < deadline) {
201
+ try {
202
+ const target = context.pages().find((p) => p.url().includes("gohighlevel.com")) ?? page;
203
+ const result = await target.evaluate(EXTRACT_FIREBASE_EXPRESSION);
204
+ const found = result?.found;
205
+ if (found?.ghl_firebase_api_key && found.ghl_user_id && found.ghl_firebase_refresh_token) {
206
+ return found;
207
+ }
208
+ if (result?.poisoned && !healed) {
209
+ healed = true;
210
+ await target.evaluate(HEAL_POISONED_DB_EXPRESSION).catch(() => {
211
+ });
212
+ await target.reload({ waitUntil: "domcontentloaded", timeout: 6e4 }).catch(() => {
213
+ });
214
+ }
215
+ } catch {
216
+ }
217
+ if (context.pages().length === 0) return null;
218
+ await new Promise((r) => setTimeout(r, 2e3));
219
+ }
220
+ return null;
221
+ }
222
+ async function main() {
223
+ const startedAt = now();
224
+ writeState({ status: "waiting_login" }, startedAt);
225
+ if (profileExists()) {
226
+ try {
227
+ const ctx2 = await launchContext(true);
228
+ try {
229
+ const captured = await pollPage(ctx2, HEADLESS_PROBE_TIMEOUT_MS);
230
+ if (captured) {
231
+ writeState({ status: "captured", ...toState(captured) }, startedAt);
232
+ await ctx2.close();
233
+ return;
234
+ }
235
+ } finally {
236
+ await ctx2.close().catch(() => {
237
+ });
238
+ }
239
+ } catch {
240
+ }
241
+ }
242
+ let ctx;
243
+ try {
244
+ ctx = await launchContext(false);
245
+ } catch (e) {
246
+ writeState(
247
+ {
248
+ status: "error",
249
+ error: "Could not launch Chrome or Edge: " + (e instanceof Error ? e.message : String(e))
250
+ },
251
+ startedAt
252
+ );
253
+ return;
254
+ }
255
+ try {
256
+ writeState({ status: "waiting_login" }, startedAt);
257
+ const captured = await pollPage(ctx, HELPER_LOGIN_TIMEOUT_MS);
258
+ if (captured) {
259
+ writeState({ status: "captured", ...toState(captured) }, startedAt);
260
+ } else {
261
+ writeState({ status: "timeout" }, startedAt);
262
+ }
263
+ } catch (e) {
264
+ writeState(
265
+ { status: "error", error: e instanceof Error ? e.message : String(e) },
266
+ startedAt
267
+ );
268
+ } finally {
269
+ await ctx.close().catch(() => {
270
+ });
271
+ }
272
+ }
273
+ function toState(c) {
274
+ return {
275
+ ghl_firebase_api_key: c.ghl_firebase_api_key,
276
+ ghl_user_id: c.ghl_user_id,
277
+ ghl_firebase_refresh_token: c.ghl_firebase_refresh_token,
278
+ ...c.account_email ? { account_email: c.account_email } : {}
279
+ };
280
+ }
281
+ main().catch((e) => {
282
+ writeCaptureState({
283
+ status: "error",
284
+ error: e instanceof Error ? e.message : String(e),
285
+ startedAt: now(),
286
+ updatedAt: now()
287
+ });
288
+ });