@elitedcs/ghl-mcp 3.33.0 → 3.34.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 +35 -0
- package/README.md +23 -5
- package/dist/index.js +403 -66
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.34.0 — Headless/server deployment hardening
|
|
4
|
+
|
|
5
|
+
Built for production server installs (Linux droplets, containers, MCP
|
|
6
|
+
gateways). License scope, stated plainly: **one license covers every
|
|
7
|
+
GoHighLevel sub-account you manage — unlimited, across multiple agency
|
|
8
|
+
companies — on up to 3 of your own machines. Never metered per account.**
|
|
9
|
+
Nothing in the code ever capped registered locations; the docs now say so.
|
|
10
|
+
|
|
11
|
+
- **Headless seed CLI** behind an explicit sentinel (`ghl-mcp cli …`), so
|
|
12
|
+
gateway-supplied argv can never trigger a subcommand:
|
|
13
|
+
`register-location`, `register-company-firebase`, `register-agency-key`,
|
|
14
|
+
`list-locations`. Live validation identical to the interactive tools
|
|
15
|
+
(`--no-validate` for air-gapped builds), strict arg parsing, deterministic
|
|
16
|
+
exit codes (0/2/3/4), config-dir writability preflight, never prints keys.
|
|
17
|
+
- **`.ghl-tokens.json` schema documented** with a pre-populatable example —
|
|
18
|
+
the registry is read at boot, so a hand-seeded file just works. Full guide
|
|
19
|
+
at `docs/HEADLESS.md` (+ new README "Server / Headless Deployment" section).
|
|
20
|
+
- **Firebase scope documented:** credentials are per-COMPANY, not
|
|
21
|
+
per-sub-account — one set covers every sub-account under a company; each
|
|
22
|
+
company's token rotates and persists independently.
|
|
23
|
+
- **`GHL_MCP_CONFIG_DIR`** — explicit absolute-path config-dir override so all
|
|
24
|
+
state (including auto-rotated Firebase tokens) lives on a persistent
|
|
25
|
+
mounted volume. Relative paths fail fast at boot.
|
|
26
|
+
- **Node 20 supported** (`engines: >=20`, esbuild target lowered to node20).
|
|
27
|
+
Verified on real Node 20: full test suite, server boot, CLI, and Ed25519
|
|
28
|
+
attestation verification (one harmless ExperimentalWarning). The publish
|
|
29
|
+
smoke test now runs on a Node 20/22/24 matrix.
|
|
30
|
+
- **Graceful shutdown** on SIGTERM/SIGINT (bounded 3s) for supervised
|
|
31
|
+
deployments.
|
|
32
|
+
- **`GHL_MCP_DISABLE_UPDATE_CHECK=1`** disables the startup npm version ping
|
|
33
|
+
(air-gapped/firewalled servers).
|
|
34
|
+
- Transport remains stdio by design (gateways supervise it as a child
|
|
35
|
+
process); an optional Streamable-HTTP mode is on the roadmap for the
|
|
36
|
+
server-deployment track.
|
|
37
|
+
|
|
3
38
|
## 3.33.0 — Account-wide silent-failure audit + trust hardening
|
|
4
39
|
|
|
5
40
|
**`audit_workflows`** — scans EVERY workflow in the current location for
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Built by [Elite DCs, LLC](https://elitedcs.com).
|
|
|
26
26
|
> "Thank you, Jerry. You're a star."
|
|
27
27
|
> — Frankie B.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
30+ releases on npm since launch (v3.6.0 → v3.34.0). Bugs get fixed in days, not quarters.
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
@@ -217,7 +217,25 @@ To unlock full builder access across multiple clients from one install:
|
|
|
217
217
|
|
|
218
218
|
---
|
|
219
219
|
|
|
220
|
-
##
|
|
220
|
+
## Server / Headless Deployment
|
|
221
|
+
|
|
222
|
+
GHL Command runs headless — Linux droplet, container, or behind an MCP gateway (MetaMCP, etc.). **One license covers every GoHighLevel sub-account you manage, on up to 3 of your own machines. No per-account fees.** A server counts as one machine; restarts never consume extra activations.
|
|
223
|
+
|
|
224
|
+
Highlights (full guide: [`docs/HEADLESS.md`](docs/HEADLESS.md)):
|
|
225
|
+
|
|
226
|
+
- **Scriptable multi-account setup** — pre-seed the documented `.ghl-tokens.json`, or use the CLI in a Dockerfile/entrypoint:
|
|
227
|
+
```bash
|
|
228
|
+
npx -y @elitedcs/ghl-mcp cli register-location --location-id XXXX --api-key pit-... --name "Account"
|
|
229
|
+
npx -y @elitedcs/ghl-mcp cli register-company-firebase --company-id YYYY --refresh-token ... --user-id ...
|
|
230
|
+
npx -y @elitedcs/ghl-mcp cli register-agency-key --api-key pit-...
|
|
231
|
+
```
|
|
232
|
+
- **Persistent config dir** — set `GHL_MCP_CONFIG_DIR=/data/ghl-mcp` (absolute path) on a mounted volume so auto-rotated Firebase tokens survive container restarts.
|
|
233
|
+
- **Firebase is per-company, not per-sub-account** — one credential set covers all sub-accounts under a company.
|
|
234
|
+
- **Node 20+**, graceful SIGTERM shutdown, `GHL_MCP_DISABLE_UPDATE_CHECK=1` for air-gapped boxes. Transport is stdio (your gateway supervises it as a child process); an HTTP serve mode is on the roadmap.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Tools
|
|
221
239
|
|
|
222
240
|
> **v3.10.0 adds Email Templates** (`list_email_templates`, `create_email_template`, `update_email_template`) — Claude can now create new HTML email templates and save content into them via the public API. Templates power both standalone marketing emails and workflow email actions. Delete + rename remain UI-only (no public-API endpoint exists).
|
|
223
241
|
|
|
@@ -648,9 +666,9 @@ This MCP server is safe to share via GitHub.
|
|
|
648
666
|
| Concern | How It's Handled |
|
|
649
667
|
|---|---|
|
|
650
668
|
| **API Keys** | Stored in a per-user credentials file (`~/Library/Application Support/elitedcs-ghl-mcp/credentials.json` on Mac) at chmod 0600. Never in code, never committed. |
|
|
651
|
-
| **License validation** |
|
|
669
|
+
| **License validation** | Checked at `setup_ghl_mcp` against the elitedcs.com license server, then cached as a signed attestation (~30 days). The server renews the attestation in the background as expiry approaches — never per-request, and your GHL keys are never sent to us. |
|
|
652
670
|
| **Multi-user** | Each user brings their own license key + GHL API key. Complete account isolation. |
|
|
653
|
-
| **Scope** | The server only talks to GHL's API
|
|
671
|
+
| **Scope** | The server only talks to GHL's API, the elitedcs.com license server (setup + periodic attestation renewal), and npm's registry (startup version banner — `GHL_MCP_DISABLE_UPDATE_CHECK=1` to disable). No filesystem access beyond its config dir, no shell commands. |
|
|
654
672
|
| **Permissions** | Controlled by your GHL Private Integration scopes. Disable what you don't need. |
|
|
655
673
|
|
|
656
674
|
---
|
|
@@ -778,7 +796,7 @@ Built by **[Elite DCs, LLC](https://elitedcs.com)** — a digital marketing and
|
|
|
778
796
|
|
|
779
797
|
**Tech stack:** TypeScript, Node.js, esbuild, MCP SDK, Zod, GHL API v2, Firebase Auth
|
|
780
798
|
|
|
781
|
-
**Version:**
|
|
799
|
+
**Version:** 3.34.0
|
|
782
800
|
|
|
783
801
|
---
|
|
784
802
|
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ 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.34.0",
|
|
35
35
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
36
36
|
description: "GoHighLevel MCP Server for Claude. 218 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
37
37
|
main: "dist/index.js",
|
|
@@ -47,7 +47,7 @@ var require_package = __commonJS({
|
|
|
47
47
|
"CHANGELOG.md"
|
|
48
48
|
],
|
|
49
49
|
scripts: {
|
|
50
|
-
build: "esbuild src/index.ts --bundle --platform=node --target=
|
|
50
|
+
build: "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external",
|
|
51
51
|
setup: "node setup-wizard.mjs",
|
|
52
52
|
start: "node dist/index.js",
|
|
53
53
|
dev: "tsc --watch",
|
|
@@ -85,7 +85,7 @@ var require_package = __commonJS({
|
|
|
85
85
|
access: "public"
|
|
86
86
|
},
|
|
87
87
|
engines: {
|
|
88
|
-
node: ">=
|
|
88
|
+
node: ">=20"
|
|
89
89
|
},
|
|
90
90
|
dependencies: {
|
|
91
91
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
@@ -106,8 +106,8 @@ var require_package = __commonJS({
|
|
|
106
106
|
|
|
107
107
|
// src/index.ts
|
|
108
108
|
var dotenv2 = __toESM(require("dotenv"));
|
|
109
|
-
var
|
|
110
|
-
var
|
|
109
|
+
var path6 = __toESM(require("path"));
|
|
110
|
+
var fs6 = __toESM(require("fs"));
|
|
111
111
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
112
112
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
113
113
|
|
|
@@ -171,8 +171,8 @@ var GHLClient = class {
|
|
|
171
171
|
Version: version || GHL_API_VERSION
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
|
-
buildUrl(
|
|
175
|
-
const url = new URL(
|
|
174
|
+
buildUrl(path7, params) {
|
|
175
|
+
const url = new URL(path7, GHL_BASE_URL);
|
|
176
176
|
if (params) {
|
|
177
177
|
for (const [key, value] of Object.entries(params)) {
|
|
178
178
|
if (value !== void 0 && value !== null) {
|
|
@@ -182,8 +182,8 @@ var GHLClient = class {
|
|
|
182
182
|
}
|
|
183
183
|
return url.toString();
|
|
184
184
|
}
|
|
185
|
-
async request(method,
|
|
186
|
-
const url = this.buildUrl(
|
|
185
|
+
async request(method, path7, options = {}, attempt = 0) {
|
|
186
|
+
const url = this.buildUrl(path7, options.params);
|
|
187
187
|
const headers = this.buildHeaders(options.version);
|
|
188
188
|
const fetchOptions = {
|
|
189
189
|
method,
|
|
@@ -201,14 +201,14 @@ var GHLClient = class {
|
|
|
201
201
|
} catch (error) {
|
|
202
202
|
clearTimeout(timeout);
|
|
203
203
|
if (error instanceof Error && error.name === "AbortError") {
|
|
204
|
-
throw new Error(`Request timeout (30s): ${method} ${
|
|
204
|
+
throw new Error(`Request timeout (30s): ${method} ${path7}`);
|
|
205
205
|
}
|
|
206
206
|
if (!options.noRetry && attempt < MAX_RETRIES) {
|
|
207
207
|
const delay4 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
|
|
208
|
-
process.stderr.write(`[ghl-mcp] Network error on ${method} ${
|
|
208
|
+
process.stderr.write(`[ghl-mcp] Network error on ${method} ${path7}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
|
|
209
209
|
`);
|
|
210
210
|
await new Promise((r) => setTimeout(r, delay4));
|
|
211
|
-
return this.request(method,
|
|
211
|
+
return this.request(method, path7, options, attempt + 1);
|
|
212
212
|
}
|
|
213
213
|
throw error;
|
|
214
214
|
} finally {
|
|
@@ -216,10 +216,10 @@ var GHLClient = class {
|
|
|
216
216
|
}
|
|
217
217
|
if (!options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
|
|
218
218
|
const delay4 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
|
|
219
|
-
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${
|
|
219
|
+
process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path7}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
|
|
220
220
|
`);
|
|
221
221
|
await new Promise((r) => setTimeout(r, delay4));
|
|
222
|
-
return this.request(method,
|
|
222
|
+
return this.request(method, path7, options, attempt + 1);
|
|
223
223
|
}
|
|
224
224
|
if (!response.ok) {
|
|
225
225
|
let errorBody = "";
|
|
@@ -228,7 +228,7 @@ var GHLClient = class {
|
|
|
228
228
|
} catch {
|
|
229
229
|
}
|
|
230
230
|
throw new Error(
|
|
231
|
-
`GHL API Error ${response.status} ${response.statusText}: ${method} ${
|
|
231
|
+
`GHL API Error ${response.status} ${response.statusText}: ${method} ${path7}
|
|
232
232
|
${errorBody}`
|
|
233
233
|
);
|
|
234
234
|
}
|
|
@@ -240,20 +240,20 @@ ${errorBody}`
|
|
|
240
240
|
return { message: text };
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
|
-
async get(
|
|
244
|
-
return this.request("GET",
|
|
243
|
+
async get(path7, options) {
|
|
244
|
+
return this.request("GET", path7, options);
|
|
245
245
|
}
|
|
246
|
-
async post(
|
|
247
|
-
return this.request("POST",
|
|
246
|
+
async post(path7, options) {
|
|
247
|
+
return this.request("POST", path7, options);
|
|
248
248
|
}
|
|
249
|
-
async put(
|
|
250
|
-
return this.request("PUT",
|
|
249
|
+
async put(path7, options) {
|
|
250
|
+
return this.request("PUT", path7, options);
|
|
251
251
|
}
|
|
252
|
-
async patch(
|
|
253
|
-
return this.request("PATCH",
|
|
252
|
+
async patch(path7, options) {
|
|
253
|
+
return this.request("PATCH", path7, options);
|
|
254
254
|
}
|
|
255
|
-
async delete(
|
|
256
|
-
return this.request("DELETE",
|
|
255
|
+
async delete(path7, options) {
|
|
256
|
+
return this.request("DELETE", path7, options);
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
259
|
* Helper: resolves locationId from args or falls back to default
|
|
@@ -300,6 +300,15 @@ var CredentialsSchema = import_zod.z.object({
|
|
|
300
300
|
signed_attestation: import_zod.z.string().optional()
|
|
301
301
|
});
|
|
302
302
|
function appDataDir() {
|
|
303
|
+
const override = process.env.GHL_MCP_CONFIG_DIR?.trim();
|
|
304
|
+
if (override) {
|
|
305
|
+
if (!path.isAbsolute(override)) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`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.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return override;
|
|
311
|
+
}
|
|
303
312
|
const home = os.homedir();
|
|
304
313
|
if (process.platform === "darwin") {
|
|
305
314
|
return path.join(home, "Library", "Application Support", APP_NAME);
|
|
@@ -407,6 +416,7 @@ var TokenRegistryDataSchema = import_zod2.z.object({
|
|
|
407
416
|
var TokenRegistry = class _TokenRegistry {
|
|
408
417
|
data;
|
|
409
418
|
loadFailure = null;
|
|
419
|
+
lastSaveError = null;
|
|
410
420
|
filePath;
|
|
411
421
|
constructor(filePath) {
|
|
412
422
|
if (filePath) {
|
|
@@ -493,15 +503,25 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
493
503
|
fs2.chmodSync(this.filePath, 384);
|
|
494
504
|
} catch {
|
|
495
505
|
}
|
|
506
|
+
this.lastSaveError = null;
|
|
496
507
|
} catch (error) {
|
|
497
508
|
try {
|
|
498
509
|
fs2.unlinkSync(tmpPath);
|
|
499
510
|
} catch {
|
|
500
511
|
}
|
|
512
|
+
this.lastSaveError = error instanceof Error ? error.message : String(error);
|
|
501
513
|
process.stderr.write(`[ghl-mcp] Warning: Could not save token registry: ${error}
|
|
502
514
|
`);
|
|
503
515
|
}
|
|
504
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Non-null when the MOST RECENT save() failed (cleared by a later success).
|
|
519
|
+
* The seed CLI checks this after every write to turn the server's
|
|
520
|
+
* warn-and-continue policy into a hard exit code.
|
|
521
|
+
*/
|
|
522
|
+
getLastSaveError() {
|
|
523
|
+
return this.lastSaveError;
|
|
524
|
+
}
|
|
505
525
|
/**
|
|
506
526
|
* Get the API key for a specific location
|
|
507
527
|
*/
|
|
@@ -4368,16 +4388,16 @@ function registerEmailTools(server2, client) {
|
|
|
4368
4388
|
function registerEmailBuilderInternalTools(server2, builderClient) {
|
|
4369
4389
|
const client = builderClient;
|
|
4370
4390
|
if (!client) return;
|
|
4371
|
-
async function builderRequest(method,
|
|
4391
|
+
async function builderRequest(method, path7, body) {
|
|
4372
4392
|
const headers = await client.buildHeaders();
|
|
4373
|
-
const response = await fetch(`${EMAIL_BUILDER_BASE}${
|
|
4393
|
+
const response = await fetch(`${EMAIL_BUILDER_BASE}${path7}`, {
|
|
4374
4394
|
method,
|
|
4375
4395
|
headers,
|
|
4376
4396
|
body: body ? JSON.stringify(body) : void 0
|
|
4377
4397
|
});
|
|
4378
4398
|
if (!response.ok) {
|
|
4379
4399
|
const text2 = await response.text();
|
|
4380
|
-
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${
|
|
4400
|
+
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path7}
|
|
4381
4401
|
${text2}`);
|
|
4382
4402
|
}
|
|
4383
4403
|
const text = await response.text();
|
|
@@ -5634,23 +5654,23 @@ var import_zod34 = require("zod");
|
|
|
5634
5654
|
function registerFunnelBuilderTools(server2, builderClient) {
|
|
5635
5655
|
const client = builderClient;
|
|
5636
5656
|
if (!client) return;
|
|
5637
|
-
async function internalGet(
|
|
5638
|
-
return client.request("GET",
|
|
5657
|
+
async function internalGet(path7) {
|
|
5658
|
+
return client.request("GET", path7);
|
|
5639
5659
|
}
|
|
5640
|
-
async function internalPost(
|
|
5641
|
-
return client.request("POST",
|
|
5660
|
+
async function internalPost(path7, body) {
|
|
5661
|
+
return client.request("POST", path7, body);
|
|
5642
5662
|
}
|
|
5643
|
-
async function internalPut(
|
|
5644
|
-
return client.request("PUT",
|
|
5663
|
+
async function internalPut(path7, body) {
|
|
5664
|
+
return client.request("PUT", path7, body);
|
|
5645
5665
|
}
|
|
5646
|
-
async function internalDelete(
|
|
5647
|
-
return client.request("DELETE",
|
|
5666
|
+
async function internalDelete(path7) {
|
|
5667
|
+
return client.request("DELETE", path7);
|
|
5648
5668
|
}
|
|
5649
|
-
async function funnelRequest(method,
|
|
5669
|
+
async function funnelRequest(method, path7, body) {
|
|
5650
5670
|
const headers = await client.buildHeaders();
|
|
5651
5671
|
headers.Origin = "https://app.gohighlevel.com";
|
|
5652
5672
|
headers.Referer = "https://app.gohighlevel.com/";
|
|
5653
|
-
const url = `https://backend.leadconnectorhq.com/funnels${
|
|
5673
|
+
const url = `https://backend.leadconnectorhq.com/funnels${path7}`;
|
|
5654
5674
|
const options = { method, headers };
|
|
5655
5675
|
if (body && (method === "POST" || method === "PUT")) {
|
|
5656
5676
|
options.body = JSON.stringify(body);
|
|
@@ -5658,7 +5678,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
|
|
|
5658
5678
|
const response = await fetch(url, options);
|
|
5659
5679
|
if (!response.ok) {
|
|
5660
5680
|
const text2 = await response.text();
|
|
5661
|
-
throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
5681
|
+
throw new Error(`Funnel API Error ${response.status}: ${method} ${path7}
|
|
5662
5682
|
${text2}`);
|
|
5663
5683
|
}
|
|
5664
5684
|
const text = await response.text();
|
|
@@ -5950,9 +5970,9 @@ function buildUpdateFormBody(name, formData) {
|
|
|
5950
5970
|
function registerFormBuilderTools(server2, builderClient) {
|
|
5951
5971
|
const client = builderClient;
|
|
5952
5972
|
if (!client) return;
|
|
5953
|
-
async function formRequest(method,
|
|
5973
|
+
async function formRequest(method, path7, body) {
|
|
5954
5974
|
const headers = await client.buildHeaders();
|
|
5955
|
-
const url = `https://backend.leadconnectorhq.com/forms${
|
|
5975
|
+
const url = `https://backend.leadconnectorhq.com/forms${path7}`;
|
|
5956
5976
|
const options = { method, headers };
|
|
5957
5977
|
if (body && (method === "POST" || method === "PUT")) {
|
|
5958
5978
|
options.body = JSON.stringify(body);
|
|
@@ -5960,7 +5980,7 @@ function registerFormBuilderTools(server2, builderClient) {
|
|
|
5960
5980
|
const response = await fetch(url, options);
|
|
5961
5981
|
if (!response.ok) {
|
|
5962
5982
|
const text2 = await response.text();
|
|
5963
|
-
throw new Error(`Form API Error ${response.status}: ${method} ${
|
|
5983
|
+
throw new Error(`Form API Error ${response.status}: ${method} ${path7}
|
|
5964
5984
|
${text2}`);
|
|
5965
5985
|
}
|
|
5966
5986
|
const text = await response.text();
|
|
@@ -6074,10 +6094,10 @@ ${text2}`);
|
|
|
6074
6094
|
},
|
|
6075
6095
|
async ({ formId, limit, skip }) => {
|
|
6076
6096
|
try {
|
|
6077
|
-
let
|
|
6078
|
-
if (formId)
|
|
6079
|
-
if (skip)
|
|
6080
|
-
const result = await formRequest("GET",
|
|
6097
|
+
let path7 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
|
|
6098
|
+
if (formId) path7 += `&formId=${formId}`;
|
|
6099
|
+
if (skip) path7 += `&skip=${skip}`;
|
|
6100
|
+
const result = await formRequest("GET", path7);
|
|
6081
6101
|
return {
|
|
6082
6102
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
6083
6103
|
};
|
|
@@ -6093,9 +6113,9 @@ var import_zod36 = require("zod");
|
|
|
6093
6113
|
function registerPipelineBuilderTools(server2, builderClient) {
|
|
6094
6114
|
const client = builderClient;
|
|
6095
6115
|
if (!client) return;
|
|
6096
|
-
async function pipelineRequest(method,
|
|
6116
|
+
async function pipelineRequest(method, path7, body) {
|
|
6097
6117
|
const headers = await client.buildHeaders();
|
|
6098
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
6118
|
+
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path7}`;
|
|
6099
6119
|
const options = { method, headers };
|
|
6100
6120
|
if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
6101
6121
|
options.body = JSON.stringify(body);
|
|
@@ -6103,7 +6123,7 @@ function registerPipelineBuilderTools(server2, builderClient) {
|
|
|
6103
6123
|
const response = await fetch(url, options);
|
|
6104
6124
|
if (!response.ok) {
|
|
6105
6125
|
const text2 = await response.text();
|
|
6106
|
-
throw new Error(`Pipeline API Error ${response.status}: ${method} ${
|
|
6126
|
+
throw new Error(`Pipeline API Error ${response.status}: ${method} ${path7}
|
|
6107
6127
|
${text2}`);
|
|
6108
6128
|
}
|
|
6109
6129
|
const text = await response.text();
|
|
@@ -7665,9 +7685,9 @@ var OBJECT_KEYS = ["contacts", "opportunity"];
|
|
|
7665
7685
|
function registerSmartListTools(server2, builderClient) {
|
|
7666
7686
|
const client = builderClient;
|
|
7667
7687
|
if (!client) return;
|
|
7668
|
-
async function smartListRequest(method,
|
|
7688
|
+
async function smartListRequest(method, path7, body) {
|
|
7669
7689
|
const headers = await client.buildHeaders();
|
|
7670
|
-
const url = `${SMARTLIST_BASE}${
|
|
7690
|
+
const url = `${SMARTLIST_BASE}${path7}`;
|
|
7671
7691
|
const options = { method, headers };
|
|
7672
7692
|
if (body && (method === "POST" || method === "PUT")) {
|
|
7673
7693
|
options.body = JSON.stringify(body);
|
|
@@ -7675,7 +7695,7 @@ function registerSmartListTools(server2, builderClient) {
|
|
|
7675
7695
|
const response = await fetch(url, options);
|
|
7676
7696
|
if (!response.ok) {
|
|
7677
7697
|
const text2 = await response.text();
|
|
7678
|
-
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${
|
|
7698
|
+
throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path7}
|
|
7679
7699
|
${text2}`);
|
|
7680
7700
|
}
|
|
7681
7701
|
const text = await response.text();
|
|
@@ -7807,12 +7827,12 @@ var REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
|
|
|
7807
7827
|
function registerReputationTools(server2, builderClient) {
|
|
7808
7828
|
const client = builderClient;
|
|
7809
7829
|
if (!client) return;
|
|
7810
|
-
async function reputationRequest(method,
|
|
7830
|
+
async function reputationRequest(method, path7) {
|
|
7811
7831
|
const headers = await client.buildHeaders();
|
|
7812
|
-
const response = await fetch(`${REPUTATION_BASE}${
|
|
7832
|
+
const response = await fetch(`${REPUTATION_BASE}${path7}`, { method, headers });
|
|
7813
7833
|
if (!response.ok) {
|
|
7814
7834
|
const text2 = await response.text();
|
|
7815
|
-
throw new Error(`Reputation API Error ${response.status}: ${method} ${
|
|
7835
|
+
throw new Error(`Reputation API Error ${response.status}: ${method} ${path7}
|
|
7816
7836
|
${text2}`);
|
|
7817
7837
|
}
|
|
7818
7838
|
const text = await response.text();
|
|
@@ -7927,16 +7947,16 @@ var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
|
|
|
7927
7947
|
function registerMembershipTools(server2, builderClient) {
|
|
7928
7948
|
const client = builderClient;
|
|
7929
7949
|
if (!client) return;
|
|
7930
|
-
async function membershipRequest(
|
|
7950
|
+
async function membershipRequest(path7, method = "GET", body) {
|
|
7931
7951
|
const headers = await client.buildHeaders();
|
|
7932
|
-
const response = await fetch(`${MEMBERSHIP_BASE}${
|
|
7952
|
+
const response = await fetch(`${MEMBERSHIP_BASE}${path7}`, {
|
|
7933
7953
|
method,
|
|
7934
7954
|
headers,
|
|
7935
7955
|
body: body ? JSON.stringify(body) : void 0
|
|
7936
7956
|
});
|
|
7937
7957
|
if (!response.ok) {
|
|
7938
7958
|
const text2 = await response.text();
|
|
7939
|
-
throw new Error(`Membership API Error ${response.status}: ${method} ${
|
|
7959
|
+
throw new Error(`Membership API Error ${response.status}: ${method} ${path7}
|
|
7940
7960
|
${text2}`);
|
|
7941
7961
|
}
|
|
7942
7962
|
const text = await response.text();
|
|
@@ -9494,12 +9514,298 @@ function registerMetaTools(server2, installedVersion) {
|
|
|
9494
9514
|
);
|
|
9495
9515
|
}
|
|
9496
9516
|
|
|
9517
|
+
// src/cli.ts
|
|
9518
|
+
var import_node_util = require("node:util");
|
|
9519
|
+
var fs5 = __toESM(require("fs"));
|
|
9520
|
+
var path5 = __toESM(require("path"));
|
|
9521
|
+
var import_crypto2 = require("crypto");
|
|
9522
|
+
var EXIT_OK = 0;
|
|
9523
|
+
var EXIT_USAGE = 2;
|
|
9524
|
+
var EXIT_VALIDATION = 3;
|
|
9525
|
+
var EXIT_FS = 4;
|
|
9526
|
+
var USAGE = `Usage: ghl-mcp cli <subcommand> [options]
|
|
9527
|
+
|
|
9528
|
+
Subcommands:
|
|
9529
|
+
register-location Add a sub-account's Private Integration key
|
|
9530
|
+
--location-id <id> GHL Location ID (required)
|
|
9531
|
+
--api-key <pit-...> The sub-account's Private Integration key (required)
|
|
9532
|
+
--name <name> Friendly name (required)
|
|
9533
|
+
--company-id <id> Owning company ID (optional; auto-resolved when validating)
|
|
9534
|
+
--no-validate Skip the live GHL check (air-gapped; local-admin-only)
|
|
9535
|
+
|
|
9536
|
+
register-company-firebase Add a company's workflow-builder (Firebase) credentials
|
|
9537
|
+
--company-id <id> GHL company/agency ID (required; self-corrects to the
|
|
9538
|
+
token's real company when validating)
|
|
9539
|
+
--refresh-token <tok> Firebase refresh token (required)
|
|
9540
|
+
--user-id <uid> Firebase user ID (required)
|
|
9541
|
+
--api-key <AIza...> Firebase API key (optional if the home install has one)
|
|
9542
|
+
--name <name> Friendly company name (optional)
|
|
9543
|
+
--no-validate Skip the live token-mint check
|
|
9544
|
+
|
|
9545
|
+
register-agency-key Store the AGENCY-level API key (snapshots, agency reads)
|
|
9546
|
+
--api-key <pit-...> Agency-level Private Integration key (required)
|
|
9547
|
+
--no-validate Skip the live agency-scope check
|
|
9548
|
+
|
|
9549
|
+
list-locations Print registered locations / companies (never prints keys)
|
|
9550
|
+
|
|
9551
|
+
Exit codes: 0 ok, 2 usage, 3 validation failed, 4 filesystem write failed.
|
|
9552
|
+
Seed while the MCP server is stopped, or restart it afterwards.`;
|
|
9553
|
+
function errLine(msg) {
|
|
9554
|
+
process.stderr.write(msg + "\n");
|
|
9555
|
+
}
|
|
9556
|
+
function preflightWritable() {
|
|
9557
|
+
try {
|
|
9558
|
+
const dir = ensureAppDataDir();
|
|
9559
|
+
const probe = path5.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
|
|
9560
|
+
fs5.writeFileSync(probe, "ok");
|
|
9561
|
+
fs5.unlinkSync(probe);
|
|
9562
|
+
return true;
|
|
9563
|
+
} catch (error) {
|
|
9564
|
+
errLine(`Config dir is not writable: ${error instanceof Error ? error.message : String(error)}`);
|
|
9565
|
+
errLine(`Config dir resolves to the parent of: ${tokenRegistryPath()}`);
|
|
9566
|
+
errLine("If you set GHL_MCP_CONFIG_DIR, make sure it is an absolute path on a writable volume.");
|
|
9567
|
+
return false;
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
function restartReminder() {
|
|
9571
|
+
errLine("Note: if the MCP server is currently running, restart it to pick up this change");
|
|
9572
|
+
errLine("(registry writes are last-writer-wins across processes \u2014 seed stopped, or restart after).");
|
|
9573
|
+
}
|
|
9574
|
+
function confirmSaved(registry2) {
|
|
9575
|
+
const err = registry2.getLastSaveError();
|
|
9576
|
+
if (err) {
|
|
9577
|
+
errLine(`Failed to write registry: ${err}`);
|
|
9578
|
+
return false;
|
|
9579
|
+
}
|
|
9580
|
+
return true;
|
|
9581
|
+
}
|
|
9582
|
+
function parse(argv, options, required) {
|
|
9583
|
+
let parsed;
|
|
9584
|
+
try {
|
|
9585
|
+
parsed = (0, import_node_util.parseArgs)({ args: argv, options, strict: true, allowPositionals: false });
|
|
9586
|
+
} catch (error) {
|
|
9587
|
+
return { usageError: error instanceof Error ? error.message : String(error) };
|
|
9588
|
+
}
|
|
9589
|
+
for (const key of required) {
|
|
9590
|
+
const v = parsed.values[key];
|
|
9591
|
+
if (typeof v !== "string" || v.trim() === "") {
|
|
9592
|
+
return { usageError: `Missing required option: --${key}` };
|
|
9593
|
+
}
|
|
9594
|
+
}
|
|
9595
|
+
return parsed;
|
|
9596
|
+
}
|
|
9597
|
+
async function cmdRegisterLocation(argv, registry2) {
|
|
9598
|
+
const p = parse(
|
|
9599
|
+
argv,
|
|
9600
|
+
{
|
|
9601
|
+
"location-id": { type: "string" },
|
|
9602
|
+
"api-key": { type: "string" },
|
|
9603
|
+
name: { type: "string" },
|
|
9604
|
+
"company-id": { type: "string" },
|
|
9605
|
+
"no-validate": { type: "boolean" }
|
|
9606
|
+
},
|
|
9607
|
+
["location-id", "api-key", "name"]
|
|
9608
|
+
);
|
|
9609
|
+
if ("usageError" in p) {
|
|
9610
|
+
errLine(p.usageError);
|
|
9611
|
+
errLine(USAGE);
|
|
9612
|
+
return EXIT_USAGE;
|
|
9613
|
+
}
|
|
9614
|
+
const locationId2 = p.values["location-id"].trim();
|
|
9615
|
+
const apiKey2 = p.values["api-key"].trim();
|
|
9616
|
+
let name = p.values.name.trim();
|
|
9617
|
+
let companyId = p.values["company-id"]?.trim() || void 0;
|
|
9618
|
+
if (!p.values["no-validate"]) {
|
|
9619
|
+
const client = new GHLClient({ apiKey: apiKey2, locationId: locationId2 });
|
|
9620
|
+
try {
|
|
9621
|
+
const result = await client.get(`/locations/${locationId2}`);
|
|
9622
|
+
const loc = result.location ?? result;
|
|
9623
|
+
if (typeof loc?.name === "string" && loc.name) name = loc.name;
|
|
9624
|
+
if (!companyId && typeof loc?.companyId === "string") companyId = loc.companyId;
|
|
9625
|
+
} catch (error) {
|
|
9626
|
+
errLine(`Validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
9627
|
+
errLine("The key must be a Private Integration created INSIDE this sub-account, and the");
|
|
9628
|
+
errLine("Location ID must match. Use --no-validate only if this machine cannot reach GHL.");
|
|
9629
|
+
return EXIT_VALIDATION;
|
|
9630
|
+
}
|
|
9631
|
+
}
|
|
9632
|
+
if (!preflightWritable()) return EXIT_FS;
|
|
9633
|
+
try {
|
|
9634
|
+
registry2.registerLocation(locationId2, name, apiKey2, companyId);
|
|
9635
|
+
} catch (error) {
|
|
9636
|
+
errLine(`Failed to write registry: ${error instanceof Error ? error.message : String(error)}`);
|
|
9637
|
+
return EXIT_FS;
|
|
9638
|
+
}
|
|
9639
|
+
if (!confirmSaved(registry2)) return EXIT_FS;
|
|
9640
|
+
process.stdout.write(
|
|
9641
|
+
`Registered location: ${name} (${locationId2})${companyId ? ` company ${companyId}` : ""} \u2192 ${tokenRegistryPath()}
|
|
9642
|
+
`
|
|
9643
|
+
);
|
|
9644
|
+
restartReminder();
|
|
9645
|
+
return EXIT_OK;
|
|
9646
|
+
}
|
|
9647
|
+
async function cmdRegisterCompanyFirebase(argv, registry2) {
|
|
9648
|
+
const p = parse(
|
|
9649
|
+
argv,
|
|
9650
|
+
{
|
|
9651
|
+
"company-id": { type: "string" },
|
|
9652
|
+
"refresh-token": { type: "string" },
|
|
9653
|
+
"user-id": { type: "string" },
|
|
9654
|
+
"api-key": { type: "string" },
|
|
9655
|
+
name: { type: "string" },
|
|
9656
|
+
"no-validate": { type: "boolean" }
|
|
9657
|
+
},
|
|
9658
|
+
["company-id", "refresh-token", "user-id"]
|
|
9659
|
+
);
|
|
9660
|
+
if ("usageError" in p) {
|
|
9661
|
+
errLine(p.usageError);
|
|
9662
|
+
errLine(USAGE);
|
|
9663
|
+
return EXIT_USAGE;
|
|
9664
|
+
}
|
|
9665
|
+
const typedCompanyId = p.values["company-id"].trim();
|
|
9666
|
+
const refreshToken = p.values["refresh-token"].trim();
|
|
9667
|
+
const userId = p.values["user-id"].trim();
|
|
9668
|
+
const name = p.values.name?.trim();
|
|
9669
|
+
const apiKey2 = p.values["api-key"]?.trim() || process.env.GHL_FIREBASE_API_KEY || registry2.getFirebase()?.apiKey;
|
|
9670
|
+
if (!apiKey2) {
|
|
9671
|
+
errLine("No Firebase API key available. Pass --api-key (starts with 'AIza'), or seed the home");
|
|
9672
|
+
errLine("Firebase first (the key is identical across GHL accounts).");
|
|
9673
|
+
return EXIT_USAGE;
|
|
9674
|
+
}
|
|
9675
|
+
let canonicalCompanyId = typedCompanyId;
|
|
9676
|
+
if (!p.values["no-validate"]) {
|
|
9677
|
+
const fb = await validateFirebase(apiKey2, refreshToken);
|
|
9678
|
+
if (!fb.ok) {
|
|
9679
|
+
errLine(`Firebase credentials rejected: ${fb.error}`);
|
|
9680
|
+
errLine("Capture fresh values from a browser session logged into THIS company's GHL:");
|
|
9681
|
+
errLine("https://elitedcs.com/ghl-mcp-firebase");
|
|
9682
|
+
return EXIT_VALIDATION;
|
|
9683
|
+
}
|
|
9684
|
+
if (fb.companyId && fb.companyId !== typedCompanyId) {
|
|
9685
|
+
canonicalCompanyId = fb.companyId;
|
|
9686
|
+
errLine(
|
|
9687
|
+
`Note: stored under company ${fb.companyId} (the ID this token actually authenticates as), not ${typedCompanyId}.`
|
|
9688
|
+
);
|
|
9689
|
+
}
|
|
9690
|
+
} else {
|
|
9691
|
+
errLine("Warning: --no-validate stores the typed company ID verbatim. If workflow-builder calls");
|
|
9692
|
+
errLine("401 after seeding, re-run WITHOUT --no-validate so the ID self-corrects from the token.");
|
|
9693
|
+
}
|
|
9694
|
+
if (!preflightWritable()) return EXIT_FS;
|
|
9695
|
+
try {
|
|
9696
|
+
registry2.setCompanyFirebase(canonicalCompanyId, {
|
|
9697
|
+
apiKey: apiKey2,
|
|
9698
|
+
refreshToken,
|
|
9699
|
+
userId,
|
|
9700
|
+
...name ? { name } : {}
|
|
9701
|
+
});
|
|
9702
|
+
} catch (error) {
|
|
9703
|
+
errLine(`Failed to write registry: ${error instanceof Error ? error.message : String(error)}`);
|
|
9704
|
+
return EXIT_FS;
|
|
9705
|
+
}
|
|
9706
|
+
if (!confirmSaved(registry2)) return EXIT_FS;
|
|
9707
|
+
process.stdout.write(
|
|
9708
|
+
`Registered company Firebase: ${name ?? canonicalCompanyId} (${canonicalCompanyId}) \u2192 ${tokenRegistryPath()}
|
|
9709
|
+
`
|
|
9710
|
+
);
|
|
9711
|
+
restartReminder();
|
|
9712
|
+
return EXIT_OK;
|
|
9713
|
+
}
|
|
9714
|
+
async function cmdRegisterAgencyKey(argv, registry2) {
|
|
9715
|
+
const p = parse(
|
|
9716
|
+
argv,
|
|
9717
|
+
{ "api-key": { type: "string" }, "no-validate": { type: "boolean" } },
|
|
9718
|
+
["api-key"]
|
|
9719
|
+
);
|
|
9720
|
+
if ("usageError" in p) {
|
|
9721
|
+
errLine(p.usageError);
|
|
9722
|
+
errLine(USAGE);
|
|
9723
|
+
return EXIT_USAGE;
|
|
9724
|
+
}
|
|
9725
|
+
const apiKey2 = p.values["api-key"].trim();
|
|
9726
|
+
if (!p.values["no-validate"]) {
|
|
9727
|
+
const client = new GHLClient({ apiKey: apiKey2 });
|
|
9728
|
+
try {
|
|
9729
|
+
const probe = await client.get("/locations/search", { params: { limit: 1, skip: 0 } });
|
|
9730
|
+
const locations = probe?.locations;
|
|
9731
|
+
if (!Array.isArray(locations)) {
|
|
9732
|
+
throw new Error("response is not an agency location-search envelope (no locations array)");
|
|
9733
|
+
}
|
|
9734
|
+
} catch (error) {
|
|
9735
|
+
errLine(`Validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
9736
|
+
errLine("The key must be a Private Integration created at the AGENCY level (Agency Settings >");
|
|
9737
|
+
errLine("Private Integrations), with the locations scope enabled.");
|
|
9738
|
+
return EXIT_VALIDATION;
|
|
9739
|
+
}
|
|
9740
|
+
}
|
|
9741
|
+
if (!preflightWritable()) return EXIT_FS;
|
|
9742
|
+
try {
|
|
9743
|
+
registry2.setAgencyKey(apiKey2);
|
|
9744
|
+
} catch (error) {
|
|
9745
|
+
errLine(`Failed to write registry: ${error instanceof Error ? error.message : String(error)}`);
|
|
9746
|
+
return EXIT_FS;
|
|
9747
|
+
}
|
|
9748
|
+
if (!confirmSaved(registry2)) return EXIT_FS;
|
|
9749
|
+
process.stdout.write(`Registered agency key: ${apiKey2.substring(0, 12)}... \u2192 ${tokenRegistryPath()}
|
|
9750
|
+
`);
|
|
9751
|
+
restartReminder();
|
|
9752
|
+
return EXIT_OK;
|
|
9753
|
+
}
|
|
9754
|
+
function cmdListLocations(registry2) {
|
|
9755
|
+
const locs = registry2.listLocations();
|
|
9756
|
+
const companies = registry2.listCompanyFirebases();
|
|
9757
|
+
const out = {
|
|
9758
|
+
registryPath: tokenRegistryPath(),
|
|
9759
|
+
locations: locs,
|
|
9760
|
+
agencyKey: registry2.getAgencyKey() ? "registered" : "not registered",
|
|
9761
|
+
homeFirebase: registry2.getFirebase() ? "registered" : "not registered",
|
|
9762
|
+
// names/ids ONLY — no keys, tokens, or user ids (design contract).
|
|
9763
|
+
companyFirebases: companies.map(({ companyId, name }) => ({ companyId, ...name ? { name } : {} }))
|
|
9764
|
+
};
|
|
9765
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
9766
|
+
return EXIT_OK;
|
|
9767
|
+
}
|
|
9768
|
+
async function runCli(subcommand, argv) {
|
|
9769
|
+
let registry2;
|
|
9770
|
+
try {
|
|
9771
|
+
registry2 = new TokenRegistry();
|
|
9772
|
+
} catch (error) {
|
|
9773
|
+
errLine(error instanceof Error ? error.message : String(error));
|
|
9774
|
+
return EXIT_USAGE;
|
|
9775
|
+
}
|
|
9776
|
+
const loadFailure = registry2.getLoadFailure();
|
|
9777
|
+
if (loadFailure) {
|
|
9778
|
+
errLine(loadFailure);
|
|
9779
|
+
return EXIT_FS;
|
|
9780
|
+
}
|
|
9781
|
+
switch (subcommand) {
|
|
9782
|
+
case "register-location":
|
|
9783
|
+
return cmdRegisterLocation(argv, registry2);
|
|
9784
|
+
case "register-company-firebase":
|
|
9785
|
+
return cmdRegisterCompanyFirebase(argv, registry2);
|
|
9786
|
+
case "register-agency-key":
|
|
9787
|
+
return cmdRegisterAgencyKey(argv, registry2);
|
|
9788
|
+
case "list-locations":
|
|
9789
|
+
return cmdListLocations(registry2);
|
|
9790
|
+
case void 0:
|
|
9791
|
+
case "help":
|
|
9792
|
+
case "--help":
|
|
9793
|
+
case "-h":
|
|
9794
|
+
process.stdout.write(USAGE + "\n");
|
|
9795
|
+
return subcommand === void 0 ? EXIT_USAGE : EXIT_OK;
|
|
9796
|
+
default:
|
|
9797
|
+
errLine(`Unknown subcommand: ${subcommand}`);
|
|
9798
|
+
errLine(USAGE);
|
|
9799
|
+
return EXIT_USAGE;
|
|
9800
|
+
}
|
|
9801
|
+
}
|
|
9802
|
+
|
|
9497
9803
|
// src/index.ts
|
|
9498
9804
|
var bundledPkg = require_package();
|
|
9499
9805
|
var pkg = (() => {
|
|
9500
9806
|
try {
|
|
9501
9807
|
const onDisk = JSON.parse(
|
|
9502
|
-
|
|
9808
|
+
fs6.readFileSync(path6.resolve(__dirname, "..", "package.json"), "utf8")
|
|
9503
9809
|
);
|
|
9504
9810
|
if (typeof onDisk.version === "string" && onDisk.version.length > 0) {
|
|
9505
9811
|
return { version: onDisk.version };
|
|
@@ -9509,25 +9815,35 @@ var pkg = (() => {
|
|
|
9509
9815
|
return bundledPkg;
|
|
9510
9816
|
})();
|
|
9511
9817
|
dotenv2.config();
|
|
9818
|
+
{
|
|
9819
|
+
const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
|
|
9820
|
+
if (configDirOverride && !path6.isAbsolute(configDirOverride)) {
|
|
9821
|
+
process.stderr.write(
|
|
9822
|
+
`[ghl-mcp] GHL_MCP_CONFIG_DIR must be an absolute path (got "${configDirOverride}"). Use e.g. /data/ghl-mcp in a container, with a volume mounted at /data.
|
|
9823
|
+
`
|
|
9824
|
+
);
|
|
9825
|
+
process.exit(2);
|
|
9826
|
+
}
|
|
9827
|
+
}
|
|
9512
9828
|
process.on("unhandledRejection", (reason) => {
|
|
9513
9829
|
process.stderr.write(`[ghl-mcp] Unhandled rejection: ${reason}
|
|
9514
9830
|
`);
|
|
9515
9831
|
});
|
|
9516
9832
|
function hardenSecretFilePerms() {
|
|
9517
|
-
const repoDir =
|
|
9833
|
+
const repoDir = path6.resolve(__dirname, "..");
|
|
9518
9834
|
const candidates = [
|
|
9519
|
-
{ file:
|
|
9835
|
+
{ file: path6.join(repoDir, "start-mcp.sh"), mode: 448 },
|
|
9520
9836
|
// Legacy registry location (pre-migration); new location lives in app-data.
|
|
9521
|
-
{ file:
|
|
9837
|
+
{ file: path6.join(repoDir, ".ghl-tokens.json"), mode: 384 },
|
|
9522
9838
|
{ file: tokenRegistryPath(), mode: 384 }
|
|
9523
9839
|
];
|
|
9524
9840
|
for (const { file, mode } of candidates) {
|
|
9525
9841
|
let current;
|
|
9526
9842
|
try {
|
|
9527
|
-
if (!
|
|
9528
|
-
current =
|
|
9843
|
+
if (!fs6.existsSync(file)) continue;
|
|
9844
|
+
current = fs6.statSync(file).mode & 511;
|
|
9529
9845
|
if (current !== mode) {
|
|
9530
|
-
|
|
9846
|
+
fs6.chmodSync(file, mode);
|
|
9531
9847
|
}
|
|
9532
9848
|
} catch (error) {
|
|
9533
9849
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -9741,9 +10057,28 @@ async function checkForUpdates() {
|
|
|
9741
10057
|
}
|
|
9742
10058
|
}
|
|
9743
10059
|
async function main() {
|
|
10060
|
+
if (process.argv[2] === "cli") {
|
|
10061
|
+
process.exit(await runCli(process.argv[3], process.argv.slice(4)));
|
|
10062
|
+
}
|
|
9744
10063
|
await resolveAccessAndRegister();
|
|
9745
10064
|
const transport = new import_stdio.StdioServerTransport();
|
|
9746
10065
|
await server.connect(transport);
|
|
10066
|
+
let shuttingDown = false;
|
|
10067
|
+
const shutdown = async (signal) => {
|
|
10068
|
+
if (shuttingDown) return;
|
|
10069
|
+
shuttingDown = true;
|
|
10070
|
+
process.stderr.write(`[ghl-mcp] ${signal} received \u2014 shutting down.
|
|
10071
|
+
`);
|
|
10072
|
+
const deadline = setTimeout(() => process.exit(0), 3e3);
|
|
10073
|
+
deadline.unref();
|
|
10074
|
+
try {
|
|
10075
|
+
await server.close();
|
|
10076
|
+
} catch {
|
|
10077
|
+
}
|
|
10078
|
+
process.exit(0);
|
|
10079
|
+
};
|
|
10080
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
10081
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
9747
10082
|
if (inBootstrapMode) {
|
|
9748
10083
|
process.stderr.write(`[ghl-mcp] v${pkg.version} connected (bootstrap mode \u2014 only setup_ghl_mcp available).
|
|
9749
10084
|
`);
|
|
@@ -9752,7 +10087,9 @@ async function main() {
|
|
|
9752
10087
|
process.stderr.write(`[ghl-mcp] v${pkg.version} connected. Token registry: ${locCount} location(s).
|
|
9753
10088
|
`);
|
|
9754
10089
|
validateApiKey();
|
|
9755
|
-
|
|
10090
|
+
if (process.env.GHL_MCP_DISABLE_UPDATE_CHECK !== "1") {
|
|
10091
|
+
checkForUpdates();
|
|
10092
|
+
}
|
|
9756
10093
|
}
|
|
9757
10094
|
}
|
|
9758
10095
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.34.0",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
5
|
"description": "GoHighLevel MCP Server for Claude. 218 tools — full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"CHANGELOG.md"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "esbuild src/index.ts --bundle --platform=node --target=
|
|
19
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external",
|
|
20
20
|
"setup": "node setup-wizard.mjs",
|
|
21
21
|
"start": "node dist/index.js",
|
|
22
22
|
"dev": "tsc --watch",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"access": "public"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
57
|
-
"node": ">=
|
|
57
|
+
"node": ">=20"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.12.1",
|