@elitedcs/ghl-mcp 3.31.0 → 3.33.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 +62 -0
- package/README.md +4 -4
- package/dist/index.js +628 -247
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.33.0 — Account-wide silent-failure audit + trust hardening
|
|
4
|
+
|
|
5
|
+
**`audit_workflows`** — scans EVERY workflow in the current location for
|
|
6
|
+
references to pipelines, stages, custom fields, users, workflows, forms,
|
|
7
|
+
calendars, and surveys that don't exist — the GHL bug where one bad ID silently
|
|
8
|
+
kills that action and every action after it. Returns a prioritized report:
|
|
9
|
+
what's broken, what couldn't be scanned, what couldn't be fully verified.
|
|
10
|
+
Conservative by construction: it never reports a false break (uncertain checks
|
|
11
|
+
are `unverified`, not `error`). The same engine upgrade sharpens
|
|
12
|
+
`validate_workflow`: four opportunity action shapes (including the dominant
|
|
13
|
+
UI-native `internal_create_opportunity`), if/else condition-node custom-field
|
|
14
|
+
checks, `create_update_contact` field refs, hyphenated `task-notification`
|
|
15
|
+
nodes, and custom-field trigger conditions are now covered. 18 new unit tests;
|
|
16
|
+
verified live against 35 real workflows with zero false alarms.
|
|
17
|
+
|
|
18
|
+
**`register_agency_key`** — new tool to store the agency-level (company-scoped)
|
|
19
|
+
API key, with live validation before saving. Previously the snapshot tools
|
|
20
|
+
required an agency key that no tool could register.
|
|
21
|
+
|
|
22
|
+
**Trust + reliability hardening** (from the 2026-06-10 audit):
|
|
23
|
+
|
|
24
|
+
- List tools now attach `_pagination` (returned / limit / total / complete +
|
|
25
|
+
an explicit INCOMPLETE note) so a single page can never silently read as the
|
|
26
|
+
full dataset: `search_contacts`, `search_opportunities`,
|
|
27
|
+
`search_conversations`, `list_invoices`, `list_workflows_full`.
|
|
28
|
+
- Version is read from package.json at runtime instead of baked at build time —
|
|
29
|
+
a stale local build can no longer misreport its version.
|
|
30
|
+
- `health_check` now reports a corrupted token registry as a FAIL with recovery
|
|
31
|
+
steps (previously only visible on stderr, i.e. invisible in the Desktop App).
|
|
32
|
+
- Clearer guidance errors: missing locationId now points at `switch_location` /
|
|
33
|
+
`list_registered_locations`; missing agency key points at
|
|
34
|
+
`register_agency_key`.
|
|
35
|
+
- Firebase token-refresh URL now percent-encodes the API key (consistency).
|
|
36
|
+
- New regression tests pin that error messages never contain API keys or
|
|
37
|
+
Firebase refresh tokens.
|
|
38
|
+
|
|
39
|
+
## 3.32.0 — Account-health summary + phone reads
|
|
40
|
+
|
|
41
|
+
Three read tools. The composite is the second roadmap build and the first
|
|
42
|
+
"how's the account doing" answer (GHL has no public reporting API, confirmed by
|
|
43
|
+
probe — so this composes existing reads).
|
|
44
|
+
|
|
45
|
+
- **`get_account_health_summary`** — one call returns, for a location: total
|
|
46
|
+
contacts + NEW contacts in a window (default 30d), total opportunities + counts
|
|
47
|
+
by status (open/won/lost/abandoned), total conversations, and phone-number
|
|
48
|
+
count.
|
|
49
|
+
- **`list_phone_numbers`** — provisioned LC Phone numbers (sid, number, label).
|
|
50
|
+
- **`list_number_pools`** — configured number pools.
|
|
51
|
+
|
|
52
|
+
**Honesty by construction.** Every metric is explicitly labeled `scope`
|
|
53
|
+
(`all_time` vs `window`, with start/end on windowed ones) so an all-time number
|
|
54
|
+
can never be read as a recent one. Any metric that can't be read returns
|
|
55
|
+
`{status:"unavailable", reason}` — never a misleading `0`. Each sub-read is
|
|
56
|
+
isolated, so one failure degrades only its own section, not the whole summary.
|
|
57
|
+
|
|
58
|
+
**Scope (verified against the live API):** windowed *new contacts* use the
|
|
59
|
+
`/contacts/search` `dateAdded` range filter; opportunity status counts use
|
|
60
|
+
filtered `meta.total`. Conversations are all-time only (the API's
|
|
61
|
+
`startAfterDate` is a cursor, not a count filter). Revenue (transactions are
|
|
62
|
+
403 for sub-account tokens) and appointments (no location-wide events endpoint)
|
|
63
|
+
are intentionally excluded.
|
|
64
|
+
|
|
3
65
|
## 3.31.0 — Snapshots: list + share-link (agency tooling)
|
|
4
66
|
|
|
5
67
|
Two new agency-level tools, the first build toward removing manual steps from the
|
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.** 219 tools across 43 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** —
|
|
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 (219 tools total with the optional Workflow Builder Firebase add-on).
|
|
120
120
|
|
|
121
121
|
### 4. Try it
|
|
122
122
|
|
|
@@ -537,7 +537,7 @@ To unlock full builder access across multiple clients from one install:
|
|
|
537
537
|
Uses **esbuild** for production builds (not tsc). TypeScript 5.9.3 + MCP SDK types cause tsc to run out of memory at 4GB+. esbuild bundles in ~5ms with zero memory issues.
|
|
538
538
|
|
|
539
539
|
```bash
|
|
540
|
-
npm run build # esbuild → dist/index.js (~
|
|
540
|
+
npm run build # esbuild → dist/index.js (~428KB)
|
|
541
541
|
npm run dev # tsc --watch (type-checking only)
|
|
542
542
|
```
|
|
543
543
|
|
|
@@ -716,7 +716,7 @@ Source repo is private. Contributors need an invitation from `drjerryrelth`. The
|
|
|
716
716
|
|
|
717
717
|
### Reducing context / token usage
|
|
718
718
|
|
|
719
|
-
Every registered MCP tool's schema is shipped to the model on every message. With
|
|
719
|
+
Every registered MCP tool's schema is shipped to the model on every message. With 219 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`:
|
|
720
720
|
|
|
721
721
|
```jsonc
|
|
722
722
|
// Claude Desktop config — enable whole modules
|
package/dist/index.js
CHANGED
|
@@ -31,9 +31,9 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "@elitedcs/ghl-mcp",
|
|
34
|
-
version: "3.
|
|
34
|
+
version: "3.33.0",
|
|
35
35
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
36
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
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",
|
|
38
38
|
bin: {
|
|
39
39
|
"ghl-mcp": "dist/index.js"
|
|
@@ -262,7 +262,7 @@ ${errorBody}`
|
|
|
262
262
|
const id = providedId || this.defaultLocationId;
|
|
263
263
|
if (!id) {
|
|
264
264
|
throw new Error(
|
|
265
|
-
"locationId is required. Provide it as a parameter or set GHL_LOCATION_ID in your .env file."
|
|
265
|
+
"locationId is required. Provide it as a parameter, run switch_location to pick a registered sub-account (list_registered_locations shows what's registered), or set GHL_LOCATION_ID in your .env file."
|
|
266
266
|
);
|
|
267
267
|
}
|
|
268
268
|
return id;
|
|
@@ -406,6 +406,7 @@ var TokenRegistryDataSchema = import_zod2.z.object({
|
|
|
406
406
|
});
|
|
407
407
|
var TokenRegistry = class _TokenRegistry {
|
|
408
408
|
data;
|
|
409
|
+
loadFailure = null;
|
|
409
410
|
filePath;
|
|
410
411
|
constructor(filePath) {
|
|
411
412
|
if (filePath) {
|
|
@@ -454,18 +455,28 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
454
455
|
return TokenRegistryDataSchema.parse(JSON.parse(raw));
|
|
455
456
|
}
|
|
456
457
|
} catch (error) {
|
|
458
|
+
let backupNote = "backup also failed \u2014 the corrupted file is still at " + this.filePath;
|
|
457
459
|
try {
|
|
458
460
|
const backupPath = this.filePath + ".corrupted." + Date.now();
|
|
459
461
|
fs2.copyFileSync(this.filePath, backupPath);
|
|
462
|
+
backupNote = `backed up to ${backupPath}`;
|
|
460
463
|
process.stderr.write(`[ghl-mcp] ERROR: Token registry corrupted. Backed up to ${backupPath}
|
|
461
464
|
`);
|
|
462
465
|
} catch {
|
|
463
466
|
}
|
|
467
|
+
this.loadFailure = `Token registry could not be loaded (${backupNote}). All sub-account registrations are unavailable this session \u2014 re-register via register_location, or restore the backup over ${this.filePath} and restart.`;
|
|
464
468
|
process.stderr.write(`[ghl-mcp] Warning: Could not load token registry: ${error}
|
|
465
469
|
`);
|
|
466
470
|
}
|
|
467
471
|
return { tokens: {} };
|
|
468
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Non-null when the registry file existed but could not be parsed at
|
|
475
|
+
* startup (we fell back to an empty registry). Surfaced by health_check.
|
|
476
|
+
*/
|
|
477
|
+
getLoadFailure() {
|
|
478
|
+
return this.loadFailure;
|
|
479
|
+
}
|
|
469
480
|
save() {
|
|
470
481
|
const tmpPath = `${this.filePath}.tmp.${process.pid}.${(0, import_crypto.randomBytes)(8).toString("hex")}`;
|
|
471
482
|
try {
|
|
@@ -503,6 +514,13 @@ var TokenRegistry = class _TokenRegistry {
|
|
|
503
514
|
getAgencyKey() {
|
|
504
515
|
return this.data.agencyKey;
|
|
505
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Store the agency/company-scoped API key (snapshots, agency-wide reads).
|
|
519
|
+
*/
|
|
520
|
+
setAgencyKey(key) {
|
|
521
|
+
this.data.agencyKey = key;
|
|
522
|
+
this.save();
|
|
523
|
+
}
|
|
506
524
|
/**
|
|
507
525
|
* Get Firebase config
|
|
508
526
|
*/
|
|
@@ -1412,7 +1430,7 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
1412
1430
|
}
|
|
1413
1431
|
}
|
|
1414
1432
|
async performTokenRefresh() {
|
|
1415
|
-
const url = `${FIREBASE_TOKEN_URL}?key=${this.firebaseApiKey}`;
|
|
1433
|
+
const url = `${FIREBASE_TOKEN_URL}?key=${encodeURIComponent(this.firebaseApiKey)}`;
|
|
1416
1434
|
const body = `grant_type=refresh_token&refresh_token=${encodeURIComponent(this.refreshToken)}`;
|
|
1417
1435
|
const response = await fetch(url, {
|
|
1418
1436
|
method: "POST",
|
|
@@ -1924,6 +1942,48 @@ function escapeRegex(str) {
|
|
|
1924
1942
|
function errorMessage(error) {
|
|
1925
1943
|
return error instanceof Error ? error.message : String(error);
|
|
1926
1944
|
}
|
|
1945
|
+
function paginationInfo(opts) {
|
|
1946
|
+
const { returned, limit, nextHint } = opts;
|
|
1947
|
+
const total = typeof opts.total === "number" ? opts.total : void 0;
|
|
1948
|
+
if (total !== void 0) {
|
|
1949
|
+
const complete = returned >= total;
|
|
1950
|
+
return {
|
|
1951
|
+
returned,
|
|
1952
|
+
limit,
|
|
1953
|
+
total,
|
|
1954
|
+
complete,
|
|
1955
|
+
...complete ? {} : {
|
|
1956
|
+
note: `INCOMPLETE: showing ${returned} of ${total} total. ${nextHint ?? "Fetch further pages before treating this as the full list."}`
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
if (returned < limit) {
|
|
1961
|
+
return { returned, limit, complete: true };
|
|
1962
|
+
}
|
|
1963
|
+
return {
|
|
1964
|
+
returned,
|
|
1965
|
+
limit,
|
|
1966
|
+
complete: "unknown",
|
|
1967
|
+
note: `POSSIBLY INCOMPLETE: returned ${returned} which hit the limit of ${limit} \u2014 more may exist. ${nextHint ?? "Fetch further pages before treating this as the full list."}`
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
function annotateListResponse(raw, listKey, limit, nextHint) {
|
|
1971
|
+
if (typeof raw !== "object" || raw === null) return raw;
|
|
1972
|
+
const obj = raw;
|
|
1973
|
+
const list = obj[listKey];
|
|
1974
|
+
if (!Array.isArray(list)) return raw;
|
|
1975
|
+
let total;
|
|
1976
|
+
const meta = obj.meta;
|
|
1977
|
+
if (typeof meta === "object" && meta !== null) {
|
|
1978
|
+
const metaTotal = meta.total;
|
|
1979
|
+
if (typeof metaTotal === "number") total = metaTotal;
|
|
1980
|
+
}
|
|
1981
|
+
if (total === void 0 && typeof obj.total === "number") total = obj.total;
|
|
1982
|
+
return {
|
|
1983
|
+
...obj,
|
|
1984
|
+
_pagination: paginationInfo({ returned: list.length, limit, total, nextHint })
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1927
1987
|
function safeTool(server2, name, description, schema, handler) {
|
|
1928
1988
|
server2.tool(name, description, schema, async (args) => {
|
|
1929
1989
|
try {
|
|
@@ -2017,7 +2077,16 @@ function registerContactTools(server2, client) {
|
|
|
2017
2077
|
order
|
|
2018
2078
|
}
|
|
2019
2079
|
});
|
|
2020
|
-
|
|
2080
|
+
const parsed = ContactSearchResponseSchema.parse(raw);
|
|
2081
|
+
return {
|
|
2082
|
+
...parsed,
|
|
2083
|
+
_pagination: paginationInfo({
|
|
2084
|
+
returned: parsed.contacts.length,
|
|
2085
|
+
limit: limit ?? 20,
|
|
2086
|
+
total: parsed.meta?.total,
|
|
2087
|
+
nextHint: "Pass startAfter + startAfterId from this response's meta to fetch the next page (both are required to advance)."
|
|
2088
|
+
})
|
|
2089
|
+
};
|
|
2021
2090
|
}
|
|
2022
2091
|
);
|
|
2023
2092
|
safeTool(
|
|
@@ -2308,7 +2377,7 @@ function registerConversationTools(server2, client) {
|
|
|
2308
2377
|
},
|
|
2309
2378
|
async ({ locationId: locationId2, contactId, assignedTo, query, status, limit, startAfterDate }) => {
|
|
2310
2379
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
2311
|
-
|
|
2380
|
+
const raw = await client.get("/conversations/search", {
|
|
2312
2381
|
params: {
|
|
2313
2382
|
locationId: resolvedLocationId,
|
|
2314
2383
|
contactId,
|
|
@@ -2319,6 +2388,12 @@ function registerConversationTools(server2, client) {
|
|
|
2319
2388
|
startAfterDate
|
|
2320
2389
|
}
|
|
2321
2390
|
});
|
|
2391
|
+
return annotateListResponse(
|
|
2392
|
+
raw,
|
|
2393
|
+
"conversations",
|
|
2394
|
+
limit ?? 20,
|
|
2395
|
+
"Pass startAfterDate from the last conversation's dateUpdated to fetch the next page."
|
|
2396
|
+
);
|
|
2322
2397
|
}
|
|
2323
2398
|
);
|
|
2324
2399
|
safeTool(
|
|
@@ -2463,7 +2538,7 @@ function registerOpportunityTools(server2, client) {
|
|
|
2463
2538
|
},
|
|
2464
2539
|
async (args) => {
|
|
2465
2540
|
const locationId2 = client.resolveLocationId(args.locationId);
|
|
2466
|
-
|
|
2541
|
+
const raw = await client.get("/opportunities/search", {
|
|
2467
2542
|
params: {
|
|
2468
2543
|
location_id: locationId2,
|
|
2469
2544
|
pipeline_id: args.pipelineId,
|
|
@@ -2480,6 +2555,12 @@ function registerOpportunityTools(server2, client) {
|
|
|
2480
2555
|
startDate: args.startDate
|
|
2481
2556
|
}
|
|
2482
2557
|
});
|
|
2558
|
+
return annotateListResponse(
|
|
2559
|
+
raw,
|
|
2560
|
+
"opportunities",
|
|
2561
|
+
args.limit ?? 20,
|
|
2562
|
+
"Pass startAfter + startAfterId from this response's meta to fetch the next page."
|
|
2563
|
+
);
|
|
2483
2564
|
}
|
|
2484
2565
|
);
|
|
2485
2566
|
safeTool(
|
|
@@ -3581,7 +3662,13 @@ function registerInvoiceTools(server2, client) {
|
|
|
3581
3662
|
if (startAt !== void 0) params.startAt = startAt;
|
|
3582
3663
|
if (endAt !== void 0) params.endAt = endAt;
|
|
3583
3664
|
if (search !== void 0) params.search = search;
|
|
3584
|
-
|
|
3665
|
+
const raw = await client.get("/invoices/", { params });
|
|
3666
|
+
return annotateListResponse(
|
|
3667
|
+
raw,
|
|
3668
|
+
"invoices",
|
|
3669
|
+
limit ?? 10,
|
|
3670
|
+
"Pass offset (current offset + limit) to fetch the next page."
|
|
3671
|
+
);
|
|
3585
3672
|
}
|
|
3586
3673
|
);
|
|
3587
3674
|
safeTool(
|
|
@@ -3617,8 +3704,8 @@ function registerInvoiceTools(server2, client) {
|
|
|
3617
3704
|
currency: import_zod17.z.string().describe("Currency code (e.g. USD)")
|
|
3618
3705
|
})).describe("Invoice line items"),
|
|
3619
3706
|
discount: import_zod17.z.object({
|
|
3620
|
-
type: import_zod17.z.string().optional(),
|
|
3621
|
-
value: import_zod17.z.number().optional()
|
|
3707
|
+
type: import_zod17.z.string().optional().describe("Discount type: 'percentage' or 'fixed'."),
|
|
3708
|
+
value: import_zod17.z.number().optional().describe("Discount amount: percent (0-100) when type=percentage, currency amount when type=fixed.")
|
|
3622
3709
|
}).optional().describe("Discount to apply"),
|
|
3623
3710
|
termsNotes: import_zod17.z.string().optional().describe("Terms and notes for the invoice"),
|
|
3624
3711
|
title: import_zod17.z.string().optional().describe("Invoice title")
|
|
@@ -3654,8 +3741,8 @@ function registerInvoiceTools(server2, client) {
|
|
|
3654
3741
|
currency: import_zod17.z.string().describe("Currency code (e.g. USD)")
|
|
3655
3742
|
})).optional().describe("Invoice line items"),
|
|
3656
3743
|
discount: import_zod17.z.object({
|
|
3657
|
-
type: import_zod17.z.string().optional(),
|
|
3658
|
-
value: import_zod17.z.number().optional()
|
|
3744
|
+
type: import_zod17.z.string().optional().describe("Discount type: 'percentage' or 'fixed'."),
|
|
3745
|
+
value: import_zod17.z.number().optional().describe("Discount amount: percent (0-100) when type=percentage, currency amount when type=fixed.")
|
|
3659
3746
|
}).optional().describe("Discount to apply"),
|
|
3660
3747
|
termsNotes: import_zod17.z.string().optional().describe("Terms and notes for the invoice"),
|
|
3661
3748
|
title: import_zod17.z.string().optional().describe("Invoice title")
|
|
@@ -5212,7 +5299,10 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
5212
5299
|
async ({ limit, skip }) => {
|
|
5213
5300
|
try {
|
|
5214
5301
|
const result = await client.listWorkflows(limit ?? 50, skip ?? 0);
|
|
5215
|
-
|
|
5302
|
+
const hint = "Pass skip (current skip + limit) to fetch the next page.";
|
|
5303
|
+
let annotated = annotateListResponse(result, "rows", limit ?? 50, hint);
|
|
5304
|
+
if (annotated === result) annotated = annotateListResponse(result, "workflows", limit ?? 50, hint);
|
|
5305
|
+
return jsonResponse(annotated);
|
|
5216
5306
|
} catch (error) {
|
|
5217
5307
|
return errorResponse(error);
|
|
5218
5308
|
}
|
|
@@ -6822,6 +6912,60 @@ The API key could not access location ${locationId2}. Make sure:
|
|
|
6822
6912
|
}
|
|
6823
6913
|
}
|
|
6824
6914
|
);
|
|
6915
|
+
server2.tool(
|
|
6916
|
+
"register_agency_key",
|
|
6917
|
+
"Store the AGENCY-level (company-scoped) API key in the token registry. This key powers agency-wide tools: list_snapshots, create_snapshot_share_link, and list_available_locations across all sub-accounts. Create it at the AGENCY level in GHL (Agency Settings > Private Integrations) \u2014 it is different from a sub-account's key. The key is validated before saving.",
|
|
6918
|
+
{
|
|
6919
|
+
apiKey: import_zod38.z.string().describe("The agency-level Private Integration API key (starts with 'pit-'). Must be created in AGENCY settings, not inside a sub-account.")
|
|
6920
|
+
},
|
|
6921
|
+
async ({ apiKey: apiKey2 }) => {
|
|
6922
|
+
if (!registry2) {
|
|
6923
|
+
return {
|
|
6924
|
+
content: [{ type: "text", text: "Token registry not available. Check .ghl-tokens.json file." }],
|
|
6925
|
+
isError: true
|
|
6926
|
+
};
|
|
6927
|
+
}
|
|
6928
|
+
const testClient = new GHLClient({ apiKey: apiKey2 });
|
|
6929
|
+
try {
|
|
6930
|
+
const probe = await testClient.get("/locations/search", { params: { limit: 1, skip: 0 } });
|
|
6931
|
+
const locations = probe?.locations;
|
|
6932
|
+
if (!Array.isArray(locations)) {
|
|
6933
|
+
throw new Error(
|
|
6934
|
+
"the key was accepted by GHL but the response is not an agency location-search envelope (no locations array) \u2014 refusing to store it as the agency key"
|
|
6935
|
+
);
|
|
6936
|
+
}
|
|
6937
|
+
} catch (error) {
|
|
6938
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6939
|
+
return {
|
|
6940
|
+
content: [
|
|
6941
|
+
{
|
|
6942
|
+
type: "text",
|
|
6943
|
+
text: `Failed to validate the agency key: ${message}
|
|
6944
|
+
|
|
6945
|
+
Make sure:
|
|
6946
|
+
1. The Private Integration was created at the AGENCY level (Agency Settings > Private Integrations), not inside a sub-account
|
|
6947
|
+
2. The key is correct (it can only be copied once when created)
|
|
6948
|
+
3. The integration has the locations scope enabled`
|
|
6949
|
+
}
|
|
6950
|
+
],
|
|
6951
|
+
isError: true
|
|
6952
|
+
};
|
|
6953
|
+
}
|
|
6954
|
+
const hadKey = Boolean(registry2.getAgencyKey());
|
|
6955
|
+
registry2.setAgencyKey(apiKey2);
|
|
6956
|
+
return {
|
|
6957
|
+
content: [
|
|
6958
|
+
{
|
|
6959
|
+
type: "text",
|
|
6960
|
+
text: `Agency key ${hadKey ? "replaced" : "registered"}: ${apiKey2.substring(0, 12)}...
|
|
6961
|
+
Saved to the token registry.
|
|
6962
|
+
|
|
6963
|
+
Agency-wide tools now available: list_snapshots, create_snapshot_share_link, and list_available_locations (across all sub-accounts).`
|
|
6964
|
+
}
|
|
6965
|
+
]
|
|
6966
|
+
};
|
|
6967
|
+
}
|
|
6968
|
+
);
|
|
6825
6969
|
server2.tool(
|
|
6826
6970
|
"unregister_location",
|
|
6827
6971
|
"Remove a GHL sub-account from the token registry.",
|
|
@@ -8345,6 +8489,11 @@ ${errors.join("\n")}` : "\nNo errors!",
|
|
|
8345
8489
|
|
|
8346
8490
|
// src/tools/validators.ts
|
|
8347
8491
|
var import_zod47 = require("zod");
|
|
8492
|
+
var ALL_CATEGORIES = ["pipeline", "stage", "custom_field", "user", "workflow", "form", "calendar", "survey"];
|
|
8493
|
+
var ID_SHAPE = /^[A-Za-z0-9]{17,}$/;
|
|
8494
|
+
function isIdShaped(s) {
|
|
8495
|
+
return typeof s === "string" && ID_SHAPE.test(s);
|
|
8496
|
+
}
|
|
8348
8497
|
function extractFromTrigger(trigger, refs) {
|
|
8349
8498
|
const triggerName = String(trigger.name ?? trigger.type ?? "unnamed trigger");
|
|
8350
8499
|
const where = `trigger "${triggerName}"`;
|
|
@@ -8354,10 +8503,21 @@ function extractFromTrigger(trigger, refs) {
|
|
|
8354
8503
|
const field = typeof c.field === "string" ? c.field : null;
|
|
8355
8504
|
const value = c.value;
|
|
8356
8505
|
if (!field || value === void 0 || value === null) continue;
|
|
8506
|
+
const childWhere = `${where} \u2192 conditions[${i}] (field=${field})`;
|
|
8507
|
+
if (field.startsWith("contact.")) {
|
|
8508
|
+
const suffix = field.slice("contact.".length);
|
|
8509
|
+
if (suffix === "assignedTo") {
|
|
8510
|
+
for (const v of Array.isArray(value) ? value : [value]) {
|
|
8511
|
+
if (isIdShaped(v)) refs.push({ kind: "user", id: v, where: childWhere });
|
|
8512
|
+
}
|
|
8513
|
+
} else if (isIdShaped(suffix)) {
|
|
8514
|
+
refs.push({ kind: "custom_field", id: suffix, where: childWhere });
|
|
8515
|
+
}
|
|
8516
|
+
continue;
|
|
8517
|
+
}
|
|
8357
8518
|
const valueAsArray = Array.isArray(value) ? value : [value];
|
|
8358
8519
|
for (const v of valueAsArray) {
|
|
8359
8520
|
if (typeof v !== "string") continue;
|
|
8360
|
-
const childWhere = `${where} \u2192 conditions[${i}] (field=${field})`;
|
|
8361
8521
|
switch (field) {
|
|
8362
8522
|
case "opportunity.pipelineId":
|
|
8363
8523
|
refs.push({ kind: "pipeline", id: v, where: childWhere });
|
|
@@ -8366,7 +8526,6 @@ function extractFromTrigger(trigger, refs) {
|
|
|
8366
8526
|
refs.push({ kind: "stage", id: v, where: childWhere });
|
|
8367
8527
|
break;
|
|
8368
8528
|
case "opportunity.assignedTo":
|
|
8369
|
-
case "contact.assignedTo":
|
|
8370
8529
|
refs.push({ kind: "user", id: v, where: childWhere });
|
|
8371
8530
|
break;
|
|
8372
8531
|
case "workflow.id":
|
|
@@ -8384,17 +8543,18 @@ function extractFromTrigger(trigger, refs) {
|
|
|
8384
8543
|
}
|
|
8385
8544
|
}
|
|
8386
8545
|
}
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8546
|
+
}
|
|
8547
|
+
function customInputId(attr, filterFields) {
|
|
8548
|
+
const arr = Array.isArray(attr.__customInputFields__) ? attr.__customInputFields__ : [];
|
|
8549
|
+
for (const raw of arr) {
|
|
8550
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
8551
|
+
const c = raw;
|
|
8552
|
+
if (typeof c.filterField === "string" && filterFields.includes(c.filterField)) {
|
|
8553
|
+
if (isIdShaped(c.secondValue)) return c.secondValue;
|
|
8554
|
+
if (isIdShaped(c.value)) return c.value;
|
|
8396
8555
|
}
|
|
8397
8556
|
}
|
|
8557
|
+
return void 0;
|
|
8398
8558
|
}
|
|
8399
8559
|
function extractFromAction(action, refs) {
|
|
8400
8560
|
const type = typeof action.type === "string" ? action.type : "unknown";
|
|
@@ -8402,258 +8562,267 @@ function extractFromAction(action, refs) {
|
|
|
8402
8562
|
const where = `action "${name}" (${type})`;
|
|
8403
8563
|
const attr = action.attributes ?? {};
|
|
8404
8564
|
switch (type) {
|
|
8405
|
-
|
|
8565
|
+
// ── Contact custom fields (only id-shaped values; standard names skipped) ──
|
|
8566
|
+
case "update_contact_field":
|
|
8567
|
+
case "create_update_contact": {
|
|
8406
8568
|
const fields = Array.isArray(attr.fields) ? attr.fields : [];
|
|
8407
8569
|
for (let i = 0; i < fields.length; i++) {
|
|
8408
8570
|
const f = fields[i];
|
|
8409
|
-
if (
|
|
8410
|
-
refs.push({
|
|
8411
|
-
kind: "custom_field",
|
|
8412
|
-
id: f.field,
|
|
8413
|
-
where: `${where} \u2192 fields[${i}]`
|
|
8414
|
-
});
|
|
8415
|
-
}
|
|
8571
|
+
if (isIdShaped(f?.field)) refs.push({ kind: "custom_field", id: f.field, where: `${where} \u2192 fields[${i}]` });
|
|
8416
8572
|
}
|
|
8417
8573
|
break;
|
|
8418
8574
|
}
|
|
8575
|
+
// ── User refs ──
|
|
8419
8576
|
case "internal_notification": {
|
|
8420
8577
|
const notif = attr.notification ?? {};
|
|
8421
|
-
if (
|
|
8422
|
-
refs.push({
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
});
|
|
8578
|
+
if (isIdShaped(notif.selectedUser))
|
|
8579
|
+
refs.push({ kind: "user", id: notif.selectedUser, where: `${where} \u2192 notification.selectedUser` });
|
|
8580
|
+
const sms = attr.sms ?? {};
|
|
8581
|
+
const smsUsers = Array.isArray(sms.selectedUser) ? sms.selectedUser : [];
|
|
8582
|
+
for (let i = 0; i < smsUsers.length; i++) {
|
|
8583
|
+
if (isIdShaped(smsUsers[i])) refs.push({ kind: "user", id: smsUsers[i], where: `${where} \u2192 sms.selectedUser[${i}]` });
|
|
8427
8584
|
}
|
|
8428
8585
|
break;
|
|
8429
8586
|
}
|
|
8430
|
-
case "task_notification":
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
id: attr.assignedTo,
|
|
8435
|
-
where: `${where} \u2192 assignedTo`
|
|
8436
|
-
});
|
|
8437
|
-
}
|
|
8587
|
+
case "task_notification":
|
|
8588
|
+
case "task-notification": {
|
|
8589
|
+
if (isIdShaped(attr.assignedTo))
|
|
8590
|
+
refs.push({ kind: "user", id: attr.assignedTo, where: `${where} \u2192 assignedTo` });
|
|
8438
8591
|
break;
|
|
8439
8592
|
}
|
|
8593
|
+
// ── Workflow refs ──
|
|
8440
8594
|
case "remove_from_workflow": {
|
|
8441
|
-
if (typeof attr.workflowId === "string") {
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
});
|
|
8457
|
-
}
|
|
8458
|
-
}
|
|
8595
|
+
if (typeof attr.workflowId === "string") refs.push({ kind: "workflow", id: attr.workflowId, where: `${where} \u2192 workflowId` });
|
|
8596
|
+
const arr = Array.isArray(attr.workflow_id) ? attr.workflow_id : [];
|
|
8597
|
+
for (let i = 0; i < arr.length; i++) if (typeof arr[i] === "string") refs.push({ kind: "workflow", id: arr[i], where: `${where} \u2192 workflow_id[${i}]` });
|
|
8598
|
+
break;
|
|
8599
|
+
}
|
|
8600
|
+
// ── Opportunity refs — FOUR distinct shapes ──
|
|
8601
|
+
case "internal_create_opportunity": {
|
|
8602
|
+
if (isIdShaped(attr.pipelineId)) refs.push({ kind: "pipeline", id: attr.pipelineId, where: `${where} \u2192 pipelineId` });
|
|
8603
|
+
const stage = customInputId(attr, ["pipelineStageId"]);
|
|
8604
|
+
if (stage) refs.push({ kind: "stage", id: stage, where: `${where} \u2192 __customInputFields__.pipelineStageId` });
|
|
8605
|
+
break;
|
|
8606
|
+
}
|
|
8607
|
+
case "create_opportunity": {
|
|
8608
|
+
if (isIdShaped(attr.pipeline_id)) refs.push({ kind: "pipeline", id: attr.pipeline_id, where: `${where} \u2192 pipeline_id` });
|
|
8609
|
+
if (isIdShaped(attr.pipeline_stage_id)) refs.push({ kind: "stage", id: attr.pipeline_stage_id, where: `${where} \u2192 pipeline_stage_id` });
|
|
8459
8610
|
break;
|
|
8460
8611
|
}
|
|
8461
8612
|
case "internal_update_opportunity": {
|
|
8462
|
-
const
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8613
|
+
const pipe = customInputId(attr, ["pipelineId"]);
|
|
8614
|
+
if (pipe) refs.push({ kind: "pipeline", id: pipe, where: `${where} \u2192 __customInputFields__.pipelineId` });
|
|
8615
|
+
const stage = customInputId(attr, ["pipelineStageId"]);
|
|
8616
|
+
if (stage) refs.push({ kind: "stage", id: stage, where: `${where} \u2192 __customInputFields__.pipelineStageId` });
|
|
8617
|
+
break;
|
|
8618
|
+
}
|
|
8619
|
+
case "find_opportunity": {
|
|
8620
|
+
const pipe = customInputId(attr, ["pipeline_id", "pipelineId"]);
|
|
8621
|
+
if (pipe) refs.push({ kind: "pipeline", id: pipe, where: `${where} \u2192 __customInputFields__.pipeline_id` });
|
|
8622
|
+
break;
|
|
8623
|
+
}
|
|
8624
|
+
// ── if_else condition node: custom-field id is conditionSubType (NOT conditionValue) ──
|
|
8625
|
+
case "if_else": {
|
|
8626
|
+
const branches = Array.isArray(attr.branches) ? attr.branches : [];
|
|
8627
|
+
for (const b of branches) {
|
|
8628
|
+
const segs = b && typeof b === "object" && Array.isArray(b.segments) ? b.segments : [];
|
|
8629
|
+
for (const s of segs) {
|
|
8630
|
+
const conds = s && typeof s === "object" && Array.isArray(s.conditions) ? s.conditions : [];
|
|
8631
|
+
for (const raw of conds) {
|
|
8632
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
8633
|
+
const c = raw;
|
|
8634
|
+
if (c.conditionType === "contact_detail" && c.conditionSubType !== "tags" && isIdShaped(c.conditionSubType)) {
|
|
8635
|
+
refs.push({ kind: "custom_field", id: c.conditionSubType, where: `${where} \u2192 if_else condition (custom field)` });
|
|
8636
|
+
}
|
|
8637
|
+
}
|
|
8470
8638
|
}
|
|
8471
8639
|
}
|
|
8472
8640
|
break;
|
|
8473
8641
|
}
|
|
8474
8642
|
}
|
|
8475
8643
|
}
|
|
8644
|
+
function collectIds(envelope, listKeys) {
|
|
8645
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8646
|
+
let arr = null;
|
|
8647
|
+
if (Array.isArray(envelope)) arr = envelope;
|
|
8648
|
+
else if (envelope && typeof envelope === "object") {
|
|
8649
|
+
const e = envelope;
|
|
8650
|
+
for (const k of listKeys) if (Array.isArray(e[k])) {
|
|
8651
|
+
arr = e[k];
|
|
8652
|
+
break;
|
|
8653
|
+
}
|
|
8654
|
+
}
|
|
8655
|
+
if (arr) for (const item of arr) {
|
|
8656
|
+
if (typeof item === "object" && item !== null) {
|
|
8657
|
+
const o = item;
|
|
8658
|
+
const id = typeof o.id === "string" ? o.id : typeof o._id === "string" ? o._id : null;
|
|
8659
|
+
if (id) ids.add(id);
|
|
8660
|
+
}
|
|
8661
|
+
}
|
|
8662
|
+
return { ids, found: arr !== null };
|
|
8663
|
+
}
|
|
8664
|
+
async function fetchAndBuildLookups(client, builderClient, locationId2, workflowExistence) {
|
|
8665
|
+
const fetches = {
|
|
8666
|
+
pipelines: client.get("/opportunities/pipelines", { params: { locationId: locationId2 } }),
|
|
8667
|
+
customFields: client.get(`/locations/${locationId2}/customFields`),
|
|
8668
|
+
users: client.get("/users/", { params: { locationId: locationId2 } }),
|
|
8669
|
+
forms: client.get("/forms/", { params: { locationId: locationId2 } }),
|
|
8670
|
+
calendars: client.get("/calendars/", { params: { locationId: locationId2 } }),
|
|
8671
|
+
surveys: client.get("/surveys/", { params: { locationId: locationId2 } })
|
|
8672
|
+
};
|
|
8673
|
+
const keys = Object.keys(fetches);
|
|
8674
|
+
const settled = await Promise.allSettled(Object.values(fetches));
|
|
8675
|
+
const data = {};
|
|
8676
|
+
const failed = /* @__PURE__ */ new Set();
|
|
8677
|
+
for (let i = 0; i < keys.length; i++) {
|
|
8678
|
+
if (settled[i].status === "fulfilled") data[keys[i]] = settled[i].value;
|
|
8679
|
+
else {
|
|
8680
|
+
data[keys[i]] = null;
|
|
8681
|
+
failed.add(keys[i]);
|
|
8682
|
+
}
|
|
8683
|
+
}
|
|
8684
|
+
const status = {
|
|
8685
|
+
pipeline: "loaded",
|
|
8686
|
+
stage: "loaded",
|
|
8687
|
+
custom_field: "loaded",
|
|
8688
|
+
user: "loaded",
|
|
8689
|
+
workflow: "loaded",
|
|
8690
|
+
form: "loaded",
|
|
8691
|
+
calendar: "loaded",
|
|
8692
|
+
survey: "loaded"
|
|
8693
|
+
};
|
|
8694
|
+
const pipelines = /* @__PURE__ */ new Set();
|
|
8695
|
+
const stages = /* @__PURE__ */ new Set();
|
|
8696
|
+
if (failed.has("pipelines")) {
|
|
8697
|
+
status.pipeline = "failed";
|
|
8698
|
+
status.stage = "failed";
|
|
8699
|
+
} else {
|
|
8700
|
+
try {
|
|
8701
|
+
const parsed = PipelinesResponseSchema.parse(data.pipelines);
|
|
8702
|
+
for (const p of parsed.pipelines) {
|
|
8703
|
+
pipelines.add(p.id);
|
|
8704
|
+
for (const s of p.stages) stages.add(s.id);
|
|
8705
|
+
}
|
|
8706
|
+
} catch {
|
|
8707
|
+
status.pipeline = "unparseable";
|
|
8708
|
+
status.stage = "unparseable";
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8711
|
+
function setFrom(key, cat, listKeys) {
|
|
8712
|
+
if (failed.has(key)) {
|
|
8713
|
+
status[cat] = "failed";
|
|
8714
|
+
return /* @__PURE__ */ new Set();
|
|
8715
|
+
}
|
|
8716
|
+
const { ids, found } = collectIds(data[key], listKeys);
|
|
8717
|
+
if (!found) status[cat] = "unparseable";
|
|
8718
|
+
return ids;
|
|
8719
|
+
}
|
|
8720
|
+
const custom_field = setFrom("customFields", "custom_field", ["customFields"]);
|
|
8721
|
+
const user = setFrom("users", "user", ["users"]);
|
|
8722
|
+
const form = setFrom("forms", "form", ["forms"]);
|
|
8723
|
+
const calendar = setFrom("calendars", "calendar", ["calendars"]);
|
|
8724
|
+
const survey = setFrom("surveys", "survey", ["surveys"]);
|
|
8725
|
+
status.workflow = workflowExistence.complete ? "loaded" : "incomplete";
|
|
8726
|
+
return { pipelines, stages, custom_field, user, workflow: workflowExistence.ids, form, calendar, survey, status };
|
|
8727
|
+
}
|
|
8728
|
+
function auditOneWorkflow(workflow, selfId, lookups) {
|
|
8729
|
+
const refs = [];
|
|
8730
|
+
const triggers = Array.isArray(workflow.triggers) ? workflow.triggers : [];
|
|
8731
|
+
for (const t of triggers) extractFromTrigger(t, refs);
|
|
8732
|
+
const actions = Array.isArray(workflow.workflowData?.templates) ? workflow.workflowData.templates : [];
|
|
8733
|
+
for (const a of actions) extractFromAction(a, refs);
|
|
8734
|
+
return refs;
|
|
8735
|
+
}
|
|
8736
|
+
function setForCategory(lookups, kind) {
|
|
8737
|
+
switch (kind) {
|
|
8738
|
+
case "pipeline":
|
|
8739
|
+
return lookups.pipelines;
|
|
8740
|
+
case "stage":
|
|
8741
|
+
return lookups.stages;
|
|
8742
|
+
default:
|
|
8743
|
+
return lookups[kind];
|
|
8744
|
+
}
|
|
8745
|
+
}
|
|
8746
|
+
function checkRefs(refs, selfId, lookups) {
|
|
8747
|
+
const findings = [];
|
|
8748
|
+
for (const ref of refs) {
|
|
8749
|
+
const st = lookups.status[ref.kind];
|
|
8750
|
+
if (st !== "loaded") {
|
|
8751
|
+
findings.push({
|
|
8752
|
+
severity: "unverified",
|
|
8753
|
+
category: ref.kind,
|
|
8754
|
+
id: ref.id,
|
|
8755
|
+
where: ref.where,
|
|
8756
|
+
message: `${ref.kind} reference could not be verified \u2014 the ${ref.kind} list ${st === "incomplete" ? "is too large to fully load" : "failed to load or was unreadable"}. Re-run; not reported as broken.`
|
|
8757
|
+
});
|
|
8758
|
+
continue;
|
|
8759
|
+
}
|
|
8760
|
+
const valid = setForCategory(lookups, ref.kind).has(ref.id);
|
|
8761
|
+
if (valid) {
|
|
8762
|
+
if (ref.kind === "workflow" && ref.id === selfId)
|
|
8763
|
+
findings.push({ severity: "warning", category: ref.kind, id: ref.id, where: ref.where, message: `${ref.kind} id "${ref.id}" is valid (self-reference).` });
|
|
8764
|
+
} else {
|
|
8765
|
+
findings.push({
|
|
8766
|
+
severity: "error",
|
|
8767
|
+
category: ref.kind,
|
|
8768
|
+
id: ref.id,
|
|
8769
|
+
where: ref.where,
|
|
8770
|
+
message: `${ref.kind} id "${ref.id}" does not exist in this location \u2014 GHL will silently skip this action and all subsequent actions when the workflow runs.`
|
|
8771
|
+
});
|
|
8772
|
+
}
|
|
8773
|
+
}
|
|
8774
|
+
return findings;
|
|
8775
|
+
}
|
|
8776
|
+
async function fullWorkflowCatalog(builderClient) {
|
|
8777
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8778
|
+
const rows = [];
|
|
8779
|
+
const limit = 100;
|
|
8780
|
+
let skip = 0;
|
|
8781
|
+
let complete = true;
|
|
8782
|
+
for (let page = 0; page < 50; page++) {
|
|
8783
|
+
const resp = await builderClient.listWorkflows(limit, skip);
|
|
8784
|
+
const found = Array.isArray(resp?.rows) || Array.isArray(resp?.workflows) || Array.isArray(resp);
|
|
8785
|
+
if (!found) {
|
|
8786
|
+
if (page === 0) throw new Error("Could not read the workflow list (unexpected response shape) \u2014 try again.");
|
|
8787
|
+
complete = false;
|
|
8788
|
+
break;
|
|
8789
|
+
}
|
|
8790
|
+
const arr = Array.isArray(resp?.rows) ? resp.rows : Array.isArray(resp?.workflows) ? resp.workflows : Array.isArray(resp) ? resp : [];
|
|
8791
|
+
for (const w of arr) {
|
|
8792
|
+
if (w && typeof w === "object") {
|
|
8793
|
+
const o = w;
|
|
8794
|
+
const id = typeof o._id === "string" ? o._id : typeof o.id === "string" ? o.id : null;
|
|
8795
|
+
if (id) {
|
|
8796
|
+
ids.add(id);
|
|
8797
|
+
rows.push({ id, name: typeof o.name === "string" ? o.name : id });
|
|
8798
|
+
}
|
|
8799
|
+
}
|
|
8800
|
+
}
|
|
8801
|
+
if (arr.length < limit) break;
|
|
8802
|
+
skip += limit;
|
|
8803
|
+
if (page === 49) complete = false;
|
|
8804
|
+
}
|
|
8805
|
+
return { ids, rows, complete };
|
|
8806
|
+
}
|
|
8476
8807
|
function registerValidatorTools(server2, client, builderClient) {
|
|
8477
8808
|
if (!builderClient) return;
|
|
8478
8809
|
server2.tool(
|
|
8479
8810
|
"validate_workflow",
|
|
8480
|
-
"Pre-flight ID validation for
|
|
8481
|
-
{
|
|
8482
|
-
workflowId: import_zod47.z.string().describe("The workflow ID to validate.")
|
|
8483
|
-
},
|
|
8811
|
+
"Pre-flight ID validation for ONE deployed GHL workflow. Scans every trigger and action for references to pipelines, pipeline stages, custom fields, users, workflows, forms, calendars, and surveys; verifies each ID exists in the current location. Use BEFORE publish_workflow when a workflow was edited, or when a published workflow stops behaving. Catches the silent-failure bug where invalid IDs make GHL skip all subsequent actions. Never reports a false break \u2014 anything it cannot fully verify is marked 'unverified', not 'error'.",
|
|
8812
|
+
{ workflowId: import_zod47.z.string().describe("The workflow ID to validate.") },
|
|
8484
8813
|
async ({ workflowId }) => {
|
|
8485
8814
|
try {
|
|
8486
|
-
let collectIds2 = function(envelope, listKey) {
|
|
8487
|
-
const ids = /* @__PURE__ */ new Set();
|
|
8488
|
-
if (envelope && typeof envelope === "object") {
|
|
8489
|
-
const e = envelope;
|
|
8490
|
-
const arr = Array.isArray(e[listKey]) ? e[listKey] : Array.isArray(envelope) ? envelope : [];
|
|
8491
|
-
for (const item of arr) {
|
|
8492
|
-
if (typeof item === "object" && item !== null) {
|
|
8493
|
-
const o = item;
|
|
8494
|
-
const id = typeof o.id === "string" ? o.id : typeof o._id === "string" ? o._id : null;
|
|
8495
|
-
if (id) ids.add(id);
|
|
8496
|
-
}
|
|
8497
|
-
}
|
|
8498
|
-
}
|
|
8499
|
-
return ids;
|
|
8500
|
-
};
|
|
8501
|
-
var collectIds = collectIds2;
|
|
8502
8815
|
const workflow = await builderClient.getWorkflow(workflowId);
|
|
8503
8816
|
if (!workflow) return errorResponse(new Error(`Workflow ${workflowId} not found`));
|
|
8504
8817
|
const refs = [];
|
|
8505
|
-
const
|
|
8506
|
-
for (const
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
const
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
if (refs.length === 0) {
|
|
8514
|
-
const empty = {
|
|
8515
|
-
workflowId,
|
|
8516
|
-
workflowName: workflow.name,
|
|
8517
|
-
status: "ok",
|
|
8518
|
-
references_scanned: 0,
|
|
8519
|
-
issues_count: 0,
|
|
8520
|
-
findings: []
|
|
8521
|
-
};
|
|
8522
|
-
return jsonResponse(empty);
|
|
8523
|
-
}
|
|
8524
|
-
const refCategories = new Set(refs.map((r) => r.kind));
|
|
8525
|
-
const locationId2 = client.defaultLocationId;
|
|
8526
|
-
const fetches = {};
|
|
8527
|
-
if (refCategories.has("pipeline") || refCategories.has("stage")) {
|
|
8528
|
-
fetches.pipelines = client.get("/opportunities/pipelines", { params: { locationId: locationId2 } });
|
|
8529
|
-
}
|
|
8530
|
-
if (refCategories.has("custom_field")) {
|
|
8531
|
-
fetches.customFields = client.get(`/locations/${locationId2}/customFields`);
|
|
8532
|
-
}
|
|
8533
|
-
if (refCategories.has("user")) {
|
|
8534
|
-
fetches.users = client.get("/users/", { params: { locationId: locationId2 } });
|
|
8535
|
-
}
|
|
8536
|
-
if (refCategories.has("workflow")) {
|
|
8537
|
-
fetches.workflows = builderClient.listWorkflows(200);
|
|
8538
|
-
}
|
|
8539
|
-
if (refCategories.has("form")) {
|
|
8540
|
-
fetches.forms = client.get("/forms/", { params: { locationId: locationId2 } });
|
|
8541
|
-
}
|
|
8542
|
-
if (refCategories.has("calendar")) {
|
|
8543
|
-
fetches.calendars = client.get("/calendars/", { params: { locationId: locationId2 } });
|
|
8544
|
-
}
|
|
8545
|
-
if (refCategories.has("survey")) {
|
|
8546
|
-
fetches.surveys = client.get("/surveys/", { params: { locationId: locationId2 } });
|
|
8547
|
-
}
|
|
8548
|
-
const results = await Promise.allSettled(Object.values(fetches));
|
|
8549
|
-
const keys = Object.keys(fetches);
|
|
8550
|
-
const data = {};
|
|
8551
|
-
for (let i = 0; i < keys.length; i++) {
|
|
8552
|
-
const r = results[i];
|
|
8553
|
-
data[keys[i]] = r.status === "fulfilled" ? r.value : null;
|
|
8554
|
-
}
|
|
8555
|
-
const validPipelineIds = /* @__PURE__ */ new Set();
|
|
8556
|
-
const validStageIds = /* @__PURE__ */ new Set();
|
|
8557
|
-
const stageToPipeline = /* @__PURE__ */ new Map();
|
|
8558
|
-
if (data.pipelines) {
|
|
8559
|
-
try {
|
|
8560
|
-
const parsed = PipelinesResponseSchema.parse(data.pipelines);
|
|
8561
|
-
for (const p of parsed.pipelines) {
|
|
8562
|
-
validPipelineIds.add(p.id);
|
|
8563
|
-
for (const s of p.stages) {
|
|
8564
|
-
validStageIds.add(s.id);
|
|
8565
|
-
stageToPipeline.set(s.id, p.id);
|
|
8566
|
-
}
|
|
8567
|
-
}
|
|
8568
|
-
} catch {
|
|
8569
|
-
}
|
|
8570
|
-
}
|
|
8571
|
-
const validCustomFieldIds = /* @__PURE__ */ new Set();
|
|
8572
|
-
if (data.customFields && typeof data.customFields === "object") {
|
|
8573
|
-
const cf = data.customFields;
|
|
8574
|
-
const arr = Array.isArray(cf.customFields) ? cf.customFields : Array.isArray(cf) ? cf : [];
|
|
8575
|
-
for (const f of arr) {
|
|
8576
|
-
if (typeof f === "object" && f !== null && typeof f.id === "string") {
|
|
8577
|
-
validCustomFieldIds.add(f.id);
|
|
8578
|
-
}
|
|
8579
|
-
}
|
|
8580
|
-
}
|
|
8581
|
-
const validUserIds = /* @__PURE__ */ new Set();
|
|
8582
|
-
if (data.users && typeof data.users === "object") {
|
|
8583
|
-
const u = data.users;
|
|
8584
|
-
const arr = Array.isArray(u.users) ? u.users : Array.isArray(u) ? u : [];
|
|
8585
|
-
for (const user of arr) {
|
|
8586
|
-
if (typeof user === "object" && user !== null && typeof user.id === "string") {
|
|
8587
|
-
validUserIds.add(user.id);
|
|
8588
|
-
}
|
|
8589
|
-
}
|
|
8590
|
-
}
|
|
8591
|
-
const validWorkflowIds = /* @__PURE__ */ new Set();
|
|
8592
|
-
if (data.workflows && typeof data.workflows === "object") {
|
|
8593
|
-
const w = data.workflows;
|
|
8594
|
-
const arr = Array.isArray(w.rows) ? w.rows : Array.isArray(w.workflows) ? w.workflows : Array.isArray(w) ? w : [];
|
|
8595
|
-
for (const wf of arr) {
|
|
8596
|
-
if (typeof wf === "object" && wf !== null) {
|
|
8597
|
-
const o = wf;
|
|
8598
|
-
const id = typeof o._id === "string" ? o._id : typeof o.id === "string" ? o.id : null;
|
|
8599
|
-
if (id) validWorkflowIds.add(id);
|
|
8600
|
-
}
|
|
8601
|
-
}
|
|
8602
|
-
}
|
|
8603
|
-
const validFormIds = collectIds2(data.forms, "forms");
|
|
8604
|
-
const validCalendarIds = collectIds2(data.calendars, "calendars");
|
|
8605
|
-
const validSurveyIds = collectIds2(data.surveys, "surveys");
|
|
8606
|
-
const findings = [];
|
|
8607
|
-
for (const ref of refs) {
|
|
8608
|
-
let valid = false;
|
|
8609
|
-
let extraMsg = "";
|
|
8610
|
-
switch (ref.kind) {
|
|
8611
|
-
case "pipeline":
|
|
8612
|
-
valid = validPipelineIds.has(ref.id);
|
|
8613
|
-
break;
|
|
8614
|
-
case "stage":
|
|
8615
|
-
valid = validStageIds.has(ref.id);
|
|
8616
|
-
break;
|
|
8617
|
-
case "custom_field":
|
|
8618
|
-
valid = validCustomFieldIds.has(ref.id);
|
|
8619
|
-
break;
|
|
8620
|
-
case "user":
|
|
8621
|
-
valid = validUserIds.has(ref.id);
|
|
8622
|
-
break;
|
|
8623
|
-
case "workflow":
|
|
8624
|
-
valid = validWorkflowIds.has(ref.id);
|
|
8625
|
-
if (valid && ref.id === workflowId) {
|
|
8626
|
-
extraMsg = " (self-reference)";
|
|
8627
|
-
}
|
|
8628
|
-
break;
|
|
8629
|
-
case "form":
|
|
8630
|
-
valid = validFormIds.has(ref.id);
|
|
8631
|
-
break;
|
|
8632
|
-
case "calendar":
|
|
8633
|
-
valid = validCalendarIds.has(ref.id);
|
|
8634
|
-
break;
|
|
8635
|
-
case "survey":
|
|
8636
|
-
valid = validSurveyIds.has(ref.id);
|
|
8637
|
-
break;
|
|
8638
|
-
}
|
|
8639
|
-
if (!valid) {
|
|
8640
|
-
findings.push({
|
|
8641
|
-
severity: "error",
|
|
8642
|
-
category: ref.kind,
|
|
8643
|
-
id: ref.id,
|
|
8644
|
-
where: ref.where,
|
|
8645
|
-
message: `${ref.kind} id "${ref.id}" does not exist in this location \u2014 GHL will silently skip this action and all subsequent actions when the workflow runs.`
|
|
8646
|
-
});
|
|
8647
|
-
} else if (extraMsg) {
|
|
8648
|
-
findings.push({
|
|
8649
|
-
severity: "warning",
|
|
8650
|
-
category: ref.kind,
|
|
8651
|
-
id: ref.id,
|
|
8652
|
-
where: ref.where,
|
|
8653
|
-
message: `${ref.kind} id "${ref.id}" is valid${extraMsg}.`
|
|
8654
|
-
});
|
|
8655
|
-
}
|
|
8656
|
-
}
|
|
8818
|
+
for (const t of Array.isArray(workflow.triggers) ? workflow.triggers : []) extractFromTrigger(t, refs);
|
|
8819
|
+
for (const a of Array.isArray(workflow.workflowData?.templates) ? workflow.workflowData.templates : []) extractFromAction(a, refs);
|
|
8820
|
+
if (refs.length === 0)
|
|
8821
|
+
return jsonResponse({ workflowId, workflowName: workflow.name, status: "ok", references_scanned: 0, issues_count: 0, findings: [] });
|
|
8822
|
+
const needWorkflows = refs.some((r) => r.kind === "workflow");
|
|
8823
|
+
const catalog = needWorkflows ? await fullWorkflowCatalog(builderClient) : { ids: /* @__PURE__ */ new Set(), complete: true };
|
|
8824
|
+
const lookups = await fetchAndBuildLookups(client, builderClient, client.defaultLocationId, { ids: catalog.ids, complete: catalog.complete });
|
|
8825
|
+
const findings = checkRefs(refs, workflowId, lookups);
|
|
8657
8826
|
const report = {
|
|
8658
8827
|
workflowId,
|
|
8659
8828
|
workflowName: workflow.name,
|
|
@@ -8668,6 +8837,69 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
8668
8837
|
}
|
|
8669
8838
|
}
|
|
8670
8839
|
);
|
|
8840
|
+
server2.tool(
|
|
8841
|
+
"audit_workflows",
|
|
8842
|
+
"Account-wide silent-failure audit: scans EVERY workflow in the current location for references to pipelines/stages/custom-fields/users/workflows/forms/calendars/surveys that don't exist \u2014 the GHL bug where one bad ID silently kills that action and all actions after it. Returns a prioritized report of what's broken, what couldn't be scanned, and what couldn't be fully verified. Conservative: never reports a false break (uncertain checks are 'unverified', not 'broken'). Read-only.",
|
|
8843
|
+
{},
|
|
8844
|
+
async () => {
|
|
8845
|
+
try {
|
|
8846
|
+
const locationId2 = client.defaultLocationId;
|
|
8847
|
+
const catalog = await fullWorkflowCatalog(builderClient);
|
|
8848
|
+
if (catalog.rows.length === 0)
|
|
8849
|
+
return jsonResponse({ location_id: locationId2, summary: { workflows_total: 0, workflows_scanned: 0, status: "ok", message: "No workflows found in this location." }, workflows_with_issues: [], unscannable: [] });
|
|
8850
|
+
const lookups = await fetchAndBuildLookups(client, builderClient, locationId2, { ids: catalog.ids, complete: catalog.complete });
|
|
8851
|
+
const SCAN_CAP = 300;
|
|
8852
|
+
const toScan = catalog.rows.slice(0, SCAN_CAP);
|
|
8853
|
+
const CONCURRENCY = 6;
|
|
8854
|
+
const results = [];
|
|
8855
|
+
const unscannable = [];
|
|
8856
|
+
let zeroRefCount = 0;
|
|
8857
|
+
for (let i = 0; i < toScan.length; i += CONCURRENCY) {
|
|
8858
|
+
const batch = toScan.slice(i, i + CONCURRENCY);
|
|
8859
|
+
await Promise.all(batch.map(async (row) => {
|
|
8860
|
+
try {
|
|
8861
|
+
const wf = await builderClient.getWorkflow(row.id);
|
|
8862
|
+
const refs = auditOneWorkflow(wf, row.id, lookups);
|
|
8863
|
+
if (refs.length === 0) zeroRefCount++;
|
|
8864
|
+
const findings = checkRefs(refs, row.id, lookups);
|
|
8865
|
+
results.push({ id: row.id, name: wf.name ?? row.name, status: wf.status, refs: refs.length, findings });
|
|
8866
|
+
} catch (e) {
|
|
8867
|
+
unscannable.push({ id: row.id, name: row.name, reason: e instanceof Error ? e.message : String(e) });
|
|
8868
|
+
}
|
|
8869
|
+
}));
|
|
8870
|
+
}
|
|
8871
|
+
const withErrors = results.filter((r) => r.findings.some((f) => f.severity === "error")).map((r) => ({ workflowId: r.id, workflowName: r.name, status: r.status, errors: r.findings.filter((f) => f.severity === "error") })).sort((a, b) => (a.status === "published" ? -1 : 1) - (b.status === "published" ? -1 : 1));
|
|
8872
|
+
const errorsTotal = withErrors.reduce((n, w) => n + w.errors.length, 0);
|
|
8873
|
+
const unverifiedCats = ALL_CATEGORIES.filter((c) => lookups.status[c] !== "loaded");
|
|
8874
|
+
const unverifiedRefs = results.reduce((n, r) => n + r.findings.filter((f) => f.severity === "unverified").length, 0);
|
|
8875
|
+
return jsonResponse({
|
|
8876
|
+
location_id: locationId2,
|
|
8877
|
+
summary: {
|
|
8878
|
+
workflows_total: catalog.rows.length,
|
|
8879
|
+
workflows_scanned: results.length,
|
|
8880
|
+
enumeration_complete: catalog.complete,
|
|
8881
|
+
capped: catalog.rows.length > SCAN_CAP,
|
|
8882
|
+
workflows_with_errors: withErrors.length,
|
|
8883
|
+
errors_total: errorsTotal,
|
|
8884
|
+
workflows_unscannable: unscannable.length,
|
|
8885
|
+
workflows_zero_references: zeroRefCount,
|
|
8886
|
+
unverified: { categories_unloaded: unverifiedCats, references_unverified: unverifiedRefs },
|
|
8887
|
+
status: errorsTotal > 0 ? "issues_found" : "ok"
|
|
8888
|
+
},
|
|
8889
|
+
workflows_with_issues: withErrors,
|
|
8890
|
+
unscannable,
|
|
8891
|
+
notes: [
|
|
8892
|
+
...catalog.complete ? [] : ["Workflow catalog exceeded the pagination backstop \u2014 some workflow-id references shown as unverified."],
|
|
8893
|
+
...catalog.rows.length > SCAN_CAP ? [`Only the first ${SCAN_CAP} workflows were scanned (account has ${catalog.rows.length}).`] : [],
|
|
8894
|
+
...unverifiedCats.length ? [`Could not fully load: ${unverifiedCats.join(", ")} \u2014 references to those are 'unverified', not 'broken'. Re-run.`] : [],
|
|
8895
|
+
"workflow_goal, goto, and unrecognized condition types are not deeply checked in this version."
|
|
8896
|
+
]
|
|
8897
|
+
});
|
|
8898
|
+
} catch (error) {
|
|
8899
|
+
return errorResponse(error);
|
|
8900
|
+
}
|
|
8901
|
+
}
|
|
8902
|
+
);
|
|
8671
8903
|
}
|
|
8672
8904
|
|
|
8673
8905
|
// src/version-check.ts
|
|
@@ -8770,6 +9002,10 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
|
|
|
8770
9002
|
if (!registry2) {
|
|
8771
9003
|
return { name: "Token registry", status: "skip", detail: "Not initialized \u2014 using env-var credentials only. switch_location won't auto-swap keys between sub-accounts." };
|
|
8772
9004
|
}
|
|
9005
|
+
const loadFailure = registry2.getLoadFailure();
|
|
9006
|
+
if (loadFailure) {
|
|
9007
|
+
return { name: "Token registry", status: "fail", detail: loadFailure };
|
|
9008
|
+
}
|
|
8773
9009
|
const locs = registry2.listLocations();
|
|
8774
9010
|
const companies = registry2.listCompanyFirebases();
|
|
8775
9011
|
const companyNote = companies.length ? ` ${companies.length} company Firebase credential(s) registered for multi-tenant workflow-builder access.` : "";
|
|
@@ -8833,7 +9069,7 @@ function resolveSnapshotAuth(client, registry2, companyIdParam) {
|
|
|
8833
9069
|
const agencyKey = registry2?.getAgencyKey();
|
|
8834
9070
|
if (!agencyKey) {
|
|
8835
9071
|
throw new Error(
|
|
8836
|
-
"Snapshots require an agency/company-scoped API key, and none is registered. This install only has sub-account Private Integration keys.
|
|
9072
|
+
"Snapshots require an agency/company-scoped API key, and none is registered. This install only has sub-account Private Integration keys. Create a Private Integration at the AGENCY level in GHL (Agency Settings > Private Integrations), then run register_agency_key with that key, and retry."
|
|
8837
9073
|
);
|
|
8838
9074
|
}
|
|
8839
9075
|
const storedCompanyId = client.defaultLocationId ? registry2?.getToken(client.defaultLocationId)?.companyId : void 0;
|
|
@@ -8926,6 +9162,137 @@ function registerSnapshotTools(server2, client, registry2) {
|
|
|
8926
9162
|
);
|
|
8927
9163
|
}
|
|
8928
9164
|
|
|
9165
|
+
// src/tools/phone.ts
|
|
9166
|
+
var import_zod49 = require("zod");
|
|
9167
|
+
var PhoneNumberSchema = import_zod49.z.object({ sid: import_zod49.z.string(), value: import_zod49.z.string(), title: import_zod49.z.string().optional() }).passthrough();
|
|
9168
|
+
var NumbersResponseSchema = import_zod49.z.object({ phoneNumbers: import_zod49.z.array(PhoneNumberSchema) }).passthrough();
|
|
9169
|
+
var PoolsResponseSchema = import_zod49.z.object({ pools: import_zod49.z.array(import_zod49.z.object({}).passthrough()) }).passthrough();
|
|
9170
|
+
function registerPhoneTools(server2, client) {
|
|
9171
|
+
safeTool(
|
|
9172
|
+
server2,
|
|
9173
|
+
"list_phone_numbers",
|
|
9174
|
+
"List the LC Phone numbers provisioned for a location (sid, number, label). Read-only. Use to verify or count purchased numbers (e.g. provisioning step 11). Number purchase is not exposed (billable write).",
|
|
9175
|
+
{
|
|
9176
|
+
locationId: import_zod49.z.string().optional().describe("Defaults to the active location.")
|
|
9177
|
+
},
|
|
9178
|
+
async ({ locationId: locationId2 }) => {
|
|
9179
|
+
const loc = client.resolveLocationId(locationId2);
|
|
9180
|
+
const raw = await client.get("/phone-system/numbers/", { params: { locationId: loc } });
|
|
9181
|
+
const parsed = NumbersResponseSchema.parse(raw);
|
|
9182
|
+
return {
|
|
9183
|
+
locationId: loc,
|
|
9184
|
+
count: parsed.phoneNumbers.length,
|
|
9185
|
+
phoneNumbers: parsed.phoneNumbers.map((n) => ({ sid: n.sid, number: n.value, label: n.title }))
|
|
9186
|
+
};
|
|
9187
|
+
}
|
|
9188
|
+
);
|
|
9189
|
+
safeTool(
|
|
9190
|
+
server2,
|
|
9191
|
+
"list_number_pools",
|
|
9192
|
+
"List LC Phone number pools configured for a location. Read-only.",
|
|
9193
|
+
{
|
|
9194
|
+
locationId: import_zod49.z.string().optional().describe("Defaults to the active location.")
|
|
9195
|
+
},
|
|
9196
|
+
async ({ locationId: locationId2 }) => {
|
|
9197
|
+
const loc = client.resolveLocationId(locationId2);
|
|
9198
|
+
const raw = await client.get("/phone-system/number-pools/", { params: { locationId: loc } });
|
|
9199
|
+
const parsed = PoolsResponseSchema.parse(raw);
|
|
9200
|
+
return { locationId: loc, count: parsed.pools.length, pools: parsed.pools };
|
|
9201
|
+
}
|
|
9202
|
+
);
|
|
9203
|
+
}
|
|
9204
|
+
|
|
9205
|
+
// src/tools/account-health.ts
|
|
9206
|
+
var import_zod50 = require("zod");
|
|
9207
|
+
var MetaTotalSchema = import_zod50.z.object({ meta: import_zod50.z.object({ total: import_zod50.z.number() }).passthrough() }).passthrough();
|
|
9208
|
+
var TotalSchema = import_zod50.z.object({ total: import_zod50.z.number() }).passthrough();
|
|
9209
|
+
var NumbersSchema = import_zod50.z.object({ phoneNumbers: import_zod50.z.array(import_zod50.z.unknown()) }).passthrough();
|
|
9210
|
+
var OPP_STATUSES = ["open", "won", "lost", "abandoned"];
|
|
9211
|
+
async function section(scope, fn) {
|
|
9212
|
+
try {
|
|
9213
|
+
return { ...await fn(), scope, status: "ok" };
|
|
9214
|
+
} catch (e) {
|
|
9215
|
+
return { status: "unavailable", scope, reason: errorMessage(e) };
|
|
9216
|
+
}
|
|
9217
|
+
}
|
|
9218
|
+
function registerAccountHealthTools(server2, client) {
|
|
9219
|
+
safeTool(
|
|
9220
|
+
server2,
|
|
9221
|
+
"get_account_health_summary",
|
|
9222
|
+
"Account-health summary for a location, composed from existing reads (GHL has no reporting API). Returns: total contacts + NEW contacts in the window; total opportunities + counts by status (open/won/lost/abandoned); total conversations; phone-number count. Every metric is explicitly labeled all_time vs window (with start/end) \u2014 windowed and all-time numbers are never conflated. Sections that can't be read return status:'unavailable' (never a misleading 0). Revenue and appointments are intentionally excluded (not reachable / too costly via the public API).",
|
|
9223
|
+
{
|
|
9224
|
+
locationId: import_zod50.z.string().optional().describe("Defaults to the active location."),
|
|
9225
|
+
windowDays: import_zod50.z.number().int().positive().max(365).optional().describe("Lookback window in days for windowed metrics (new contacts). Default 30.")
|
|
9226
|
+
},
|
|
9227
|
+
async ({ locationId: locationId2, windowDays }) => {
|
|
9228
|
+
const loc = client.resolveLocationId(locationId2);
|
|
9229
|
+
const days = windowDays ?? 30;
|
|
9230
|
+
const end = Date.now();
|
|
9231
|
+
const start = end - days * 864e5;
|
|
9232
|
+
const startISO = new Date(start).toISOString();
|
|
9233
|
+
const endISO = new Date(end).toISOString();
|
|
9234
|
+
const [
|
|
9235
|
+
contactsAllTime,
|
|
9236
|
+
contactsNewInWindow,
|
|
9237
|
+
opportunitiesTotal,
|
|
9238
|
+
opportunitiesByStatus,
|
|
9239
|
+
conversations,
|
|
9240
|
+
phoneNumbers
|
|
9241
|
+
] = await Promise.all([
|
|
9242
|
+
section("all_time", async () => {
|
|
9243
|
+
const raw = await client.get("/contacts/", { params: { locationId: loc, limit: 1 } });
|
|
9244
|
+
return { total: MetaTotalSchema.parse(raw).meta.total };
|
|
9245
|
+
}),
|
|
9246
|
+
section("window", async () => {
|
|
9247
|
+
const raw = await client.post("/contacts/search", {
|
|
9248
|
+
body: {
|
|
9249
|
+
locationId: loc,
|
|
9250
|
+
pageLimit: 1,
|
|
9251
|
+
filters: [{ field: "dateAdded", operator: "range", value: { gte: start, lte: end } }]
|
|
9252
|
+
}
|
|
9253
|
+
});
|
|
9254
|
+
return { start: startISO, end: endISO, total: TotalSchema.parse(raw).total };
|
|
9255
|
+
}),
|
|
9256
|
+
// Overall opp total in its OWN section so a failed per-status query can't
|
|
9257
|
+
// blank a count we already have.
|
|
9258
|
+
section("all_time", async () => {
|
|
9259
|
+
const raw = await client.get("/opportunities/search", { params: { location_id: loc, limit: 1 } });
|
|
9260
|
+
return { total: MetaTotalSchema.parse(raw).meta.total };
|
|
9261
|
+
}),
|
|
9262
|
+
section("all_time", async () => {
|
|
9263
|
+
const counts = await Promise.all(
|
|
9264
|
+
OPP_STATUSES.map(async (s) => {
|
|
9265
|
+
const raw = await client.get("/opportunities/search", {
|
|
9266
|
+
params: { location_id: loc, status: s, limit: 1 }
|
|
9267
|
+
});
|
|
9268
|
+
return [s, MetaTotalSchema.parse(raw).meta.total];
|
|
9269
|
+
})
|
|
9270
|
+
);
|
|
9271
|
+
return { byStatus: Object.fromEntries(counts) };
|
|
9272
|
+
}),
|
|
9273
|
+
section("all_time", async () => {
|
|
9274
|
+
const raw = await client.get("/conversations/search", { params: { locationId: loc, limit: 1 } });
|
|
9275
|
+
return { total: TotalSchema.parse(raw).total };
|
|
9276
|
+
}),
|
|
9277
|
+
section("all_time", async () => {
|
|
9278
|
+
const raw = await client.get("/phone-system/numbers/", { params: { locationId: loc } });
|
|
9279
|
+
return { count: NumbersSchema.parse(raw).phoneNumbers.length };
|
|
9280
|
+
})
|
|
9281
|
+
]);
|
|
9282
|
+
const sections = {
|
|
9283
|
+
contactsAllTime,
|
|
9284
|
+
contactsNewInWindow,
|
|
9285
|
+
opportunitiesTotal,
|
|
9286
|
+
opportunitiesByStatus,
|
|
9287
|
+
conversations,
|
|
9288
|
+
phoneNumbers
|
|
9289
|
+
};
|
|
9290
|
+
const unavailable = Object.entries(sections).filter(([, v]) => v.status === "unavailable").map(([k]) => k);
|
|
9291
|
+
return { locationId: loc, requestedWindow: { days, start: startISO, end: endISO }, sections, unavailable };
|
|
9292
|
+
}
|
|
9293
|
+
);
|
|
9294
|
+
}
|
|
9295
|
+
|
|
8929
9296
|
// src/tools/index.ts
|
|
8930
9297
|
var publicApiTools = [
|
|
8931
9298
|
[registerContactTools, "contacts"],
|
|
@@ -8957,7 +9324,9 @@ var publicApiTools = [
|
|
|
8957
9324
|
[registerDocumentTools, "documents"],
|
|
8958
9325
|
[registerBulkOperationTools, "bulk-operations"],
|
|
8959
9326
|
[registerAccountExportTools, "account-export"],
|
|
8960
|
-
[registerTemplateDeployerTools, "template-deployer"]
|
|
9327
|
+
[registerTemplateDeployerTools, "template-deployer"],
|
|
9328
|
+
[registerPhoneTools, "phone"],
|
|
9329
|
+
[registerAccountHealthTools, "account-health"]
|
|
8961
9330
|
];
|
|
8962
9331
|
var internalApiTools = [
|
|
8963
9332
|
[registerWorkflowBuilderTools, "workflow-builder"],
|
|
@@ -9126,7 +9495,19 @@ function registerMetaTools(server2, installedVersion) {
|
|
|
9126
9495
|
}
|
|
9127
9496
|
|
|
9128
9497
|
// src/index.ts
|
|
9129
|
-
var
|
|
9498
|
+
var bundledPkg = require_package();
|
|
9499
|
+
var pkg = (() => {
|
|
9500
|
+
try {
|
|
9501
|
+
const onDisk = JSON.parse(
|
|
9502
|
+
fs5.readFileSync(path5.resolve(__dirname, "..", "package.json"), "utf8")
|
|
9503
|
+
);
|
|
9504
|
+
if (typeof onDisk.version === "string" && onDisk.version.length > 0) {
|
|
9505
|
+
return { version: onDisk.version };
|
|
9506
|
+
}
|
|
9507
|
+
} catch {
|
|
9508
|
+
}
|
|
9509
|
+
return bundledPkg;
|
|
9510
|
+
})();
|
|
9130
9511
|
dotenv2.config();
|
|
9131
9512
|
process.on("unhandledRejection", (reason) => {
|
|
9132
9513
|
process.stderr.write(`[ghl-mcp] Unhandled rejection: ${reason}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.33.0",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
|
-
"description": "GoHighLevel MCP Server for Claude.
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ghl-mcp": "dist/index.js"
|