@elitedcs/ghl-mcp 3.48.2 → 3.49.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 +25 -0
- package/README.md +4 -4
- package/dist/capture-helper.js +288 -0
- package/dist/index.js +776 -501
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.49.0 — One-click Workflow Builder unlock (`capture_firebase_interactive`)
|
|
4
|
+
|
|
5
|
+
The buyer's whole job is now: log into GHL in a Chrome window the tool opens.
|
|
6
|
+
No DevTools, no console paste, no JSON shuttling.
|
|
7
|
+
|
|
8
|
+
- **New tool `capture_firebase_interactive`** (normal mode). Spawns a detached
|
|
9
|
+
helper (`dist/capture-helper.js`) that launches the buyer's own installed
|
|
10
|
+
Chrome (playwright-core channel discovery — Edge fallback, zero browser
|
|
11
|
+
downloads) into a dedicated persistent profile, waits for a GHL login, reads
|
|
12
|
+
the three Firebase values straight out of IndexedDB (same selection logic as
|
|
13
|
+
the console script), validates them against Firebase, and saves them via the
|
|
14
|
+
same path as `enable_workflow_builder`.
|
|
15
|
+
- **Silent re-capture for token rotations.** The persistent profile keeps the
|
|
16
|
+
GHL session cookie, so every capture after the first tries HEADLESS first —
|
|
17
|
+
refresh-token rotation becomes "run the tool again, no window appears".
|
|
18
|
+
- **Timeout-proof by design.** The helper is fully detached and communicates
|
|
19
|
+
through a state file; the tool call polls with a generous window and tells
|
|
20
|
+
the buyer to re-run if they're still mid-login. An MCP client timeout can
|
|
21
|
+
never strand the flow.
|
|
22
|
+
- **Fallbacks intact.** No Chrome/Edge, closed window, or login timeout all
|
|
23
|
+
return clear instructions pointing at `auto_capture_firebase_script` (the
|
|
24
|
+
console-paste path, unchanged).
|
|
25
|
+
- Tool count: 228 → 229 (177 without Firebase).
|
|
26
|
+
|
|
27
|
+
|
|
3
28
|
## 3.48.2 — Docs patch: tool-count claims corrected (the real number is 228)
|
|
4
29
|
|
|
5
30
|
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.**
|
|
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 (
|
|
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,7 +156,7 @@ 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
|
|
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
161
|
**One-paste capture (the supported path):**
|
|
162
162
|
|
|
@@ -736,7 +736,7 @@ Source repo is private. Contributors need an invitation from `drjerryrelth`. The
|
|
|
736
736
|
|
|
737
737
|
### Reducing context / token usage
|
|
738
738
|
|
|
739
|
-
Every registered MCP tool's schema is shipped to the model on every message. With
|
|
739
|
+
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
740
|
|
|
741
741
|
```jsonc
|
|
742
742
|
// 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
|
+
});
|