@elitedcs/ghl-mcp 3.13.1 → 3.15.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 +33 -2
- package/dist/index.js +821 -437
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -31,8 +31,8 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "@elitedcs/ghl-mcp",
|
|
34
|
-
version: "3.
|
|
35
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
34
|
+
version: "3.15.0",
|
|
35
|
+
description: "GoHighLevel MCP Server for Claude. 206 tools \u2014 full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
36
36
|
main: "dist/index.js",
|
|
37
37
|
bin: {
|
|
38
38
|
"ghl-mcp": "dist/index.js"
|
|
@@ -337,15 +337,30 @@ function writeCredentials(creds) {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
// src/token-registry.ts
|
|
340
|
-
var LocationTokenSchema = import_zod2.z.object({
|
|
340
|
+
var LocationTokenSchema = import_zod2.z.object({
|
|
341
|
+
name: import_zod2.z.string(),
|
|
342
|
+
apiKey: import_zod2.z.string(),
|
|
343
|
+
// The GHL company/agency that owns this location. Routes Firebase auth:
|
|
344
|
+
// refresh tokens are company-scoped, so working in a client's sub-accounts
|
|
345
|
+
// (under a different company) requires that company's own Firebase token.
|
|
346
|
+
companyId: import_zod2.z.string().optional()
|
|
347
|
+
});
|
|
348
|
+
var FirebaseConfigSchema = import_zod2.z.object({
|
|
349
|
+
apiKey: import_zod2.z.string(),
|
|
350
|
+
refreshToken: import_zod2.z.string(),
|
|
351
|
+
userId: import_zod2.z.string()
|
|
352
|
+
});
|
|
353
|
+
var CompanyFirebaseSchema = FirebaseConfigSchema.extend({
|
|
354
|
+
name: import_zod2.z.string().optional()
|
|
355
|
+
});
|
|
341
356
|
var TokenRegistryDataSchema = import_zod2.z.object({
|
|
342
357
|
tokens: import_zod2.z.record(LocationTokenSchema),
|
|
343
358
|
agencyKey: import_zod2.z.string().optional(),
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
359
|
+
// "Home" Firebase — the company the install was set up for (credentials.json).
|
|
360
|
+
firebase: FirebaseConfigSchema.optional(),
|
|
361
|
+
// Additional companies' Firebase auth, keyed by GHL companyId. Lets ONE
|
|
362
|
+
// process operate the workflow builder across multiple clients' GHL accounts.
|
|
363
|
+
firebaseByCompany: import_zod2.z.record(CompanyFirebaseSchema).optional()
|
|
349
364
|
});
|
|
350
365
|
var TokenRegistry = class {
|
|
351
366
|
data;
|
|
@@ -453,11 +468,31 @@ var TokenRegistry = class {
|
|
|
453
468
|
return this.data.firebase;
|
|
454
469
|
}
|
|
455
470
|
/**
|
|
456
|
-
* Register a new location with its API key
|
|
471
|
+
* Register a new location with its API key. Optionally records the owning
|
|
472
|
+
* companyId (used to route Firebase auth). Re-registering preserves a
|
|
473
|
+
* previously stored companyId when a new one isn't supplied.
|
|
474
|
+
*/
|
|
475
|
+
registerLocation(locationId2, name, apiKey2, companyId) {
|
|
476
|
+
const existing = this.data.tokens[locationId2];
|
|
477
|
+
const resolvedCompanyId = companyId ?? existing?.companyId;
|
|
478
|
+
this.data.tokens[locationId2] = {
|
|
479
|
+
name,
|
|
480
|
+
apiKey: apiKey2,
|
|
481
|
+
...resolvedCompanyId ? { companyId: resolvedCompanyId } : {}
|
|
482
|
+
};
|
|
483
|
+
this.save();
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Backfill/update the companyId on an already-registered location.
|
|
487
|
+
* Returns true if a token existed and was updated.
|
|
457
488
|
*/
|
|
458
|
-
|
|
459
|
-
this.data.tokens[locationId2]
|
|
489
|
+
setLocationCompanyId(locationId2, companyId) {
|
|
490
|
+
const token = this.data.tokens[locationId2];
|
|
491
|
+
if (!token) return false;
|
|
492
|
+
if (token.companyId === companyId) return true;
|
|
493
|
+
token.companyId = companyId;
|
|
460
494
|
this.save();
|
|
495
|
+
return true;
|
|
461
496
|
}
|
|
462
497
|
/**
|
|
463
498
|
* Remove a location from the registry
|
|
@@ -480,7 +515,7 @@ var TokenRegistry = class {
|
|
|
480
515
|
}));
|
|
481
516
|
}
|
|
482
517
|
/**
|
|
483
|
-
* Update the Firebase refresh token (called on rotation)
|
|
518
|
+
* Update the home Firebase refresh token (called on rotation)
|
|
484
519
|
*/
|
|
485
520
|
updateFirebaseRefreshToken(newToken) {
|
|
486
521
|
if (this.data.firebase) {
|
|
@@ -488,6 +523,53 @@ var TokenRegistry = class {
|
|
|
488
523
|
this.save();
|
|
489
524
|
}
|
|
490
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Get a specific company's Firebase config (multi-tenant routing).
|
|
528
|
+
*/
|
|
529
|
+
getCompanyFirebase(companyId) {
|
|
530
|
+
return this.data.firebaseByCompany?.[companyId];
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Store (or replace) a company's Firebase config.
|
|
534
|
+
*/
|
|
535
|
+
setCompanyFirebase(companyId, config3) {
|
|
536
|
+
if (!this.data.firebaseByCompany) this.data.firebaseByCompany = {};
|
|
537
|
+
this.data.firebaseByCompany[companyId] = config3;
|
|
538
|
+
this.save();
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Remove a company's Firebase config. Returns true if one existed.
|
|
542
|
+
*/
|
|
543
|
+
removeCompanyFirebase(companyId) {
|
|
544
|
+
if (this.data.firebaseByCompany?.[companyId]) {
|
|
545
|
+
delete this.data.firebaseByCompany[companyId];
|
|
546
|
+
this.save();
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* List registered company Firebases WITHOUT secrets — name + userId only,
|
|
553
|
+
* safe for display in tool output.
|
|
554
|
+
*/
|
|
555
|
+
listCompanyFirebases() {
|
|
556
|
+
return Object.entries(this.data.firebaseByCompany ?? {}).map(([companyId, cfg]) => ({
|
|
557
|
+
companyId,
|
|
558
|
+
name: cfg.name,
|
|
559
|
+
userId: cfg.userId
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Update a company's Firebase refresh token (called on rotation while that
|
|
564
|
+
* company is the active one). No-op if the company isn't registered.
|
|
565
|
+
*/
|
|
566
|
+
updateCompanyFirebaseRefreshToken(companyId, newToken) {
|
|
567
|
+
const cfg = this.data.firebaseByCompany?.[companyId];
|
|
568
|
+
if (cfg) {
|
|
569
|
+
cfg.refreshToken = newToken;
|
|
570
|
+
this.save();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
491
573
|
/**
|
|
492
574
|
* Check if the registry has any tokens
|
|
493
575
|
*/
|
|
@@ -1018,11 +1100,20 @@ function validateActionChain(actions) {
|
|
|
1018
1100
|
}
|
|
1019
1101
|
}
|
|
1020
1102
|
var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
1103
|
+
// Active Firebase auth — swapped when operating in another company's GHL.
|
|
1021
1104
|
firebaseApiKey;
|
|
1022
1105
|
refreshToken;
|
|
1106
|
+
userId;
|
|
1107
|
+
currentCompanyId;
|
|
1108
|
+
// "Home" Firebase auth — the company the install was set up for. Kept so we
|
|
1109
|
+
// can restore it when switching back from a client's company.
|
|
1110
|
+
homeFirebaseApiKey;
|
|
1111
|
+
homeRefreshToken;
|
|
1112
|
+
// mutable: kept in sync when the home token rotates
|
|
1113
|
+
homeUserId;
|
|
1114
|
+
homeCompanyId;
|
|
1023
1115
|
apiKey;
|
|
1024
1116
|
locationId;
|
|
1025
|
-
userId;
|
|
1026
1117
|
registry;
|
|
1027
1118
|
cachedIdToken = null;
|
|
1028
1119
|
tokenExpiry = 0;
|
|
@@ -1030,9 +1121,14 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
1030
1121
|
constructor(config3) {
|
|
1031
1122
|
this.firebaseApiKey = config3.firebaseApiKey;
|
|
1032
1123
|
this.refreshToken = config3.refreshToken;
|
|
1124
|
+
this.userId = config3.userId;
|
|
1125
|
+
this.currentCompanyId = config3.companyId;
|
|
1126
|
+
this.homeFirebaseApiKey = config3.firebaseApiKey;
|
|
1127
|
+
this.homeRefreshToken = config3.refreshToken;
|
|
1128
|
+
this.homeUserId = config3.userId;
|
|
1129
|
+
this.homeCompanyId = config3.companyId;
|
|
1033
1130
|
this.apiKey = config3.apiKey;
|
|
1034
1131
|
this.locationId = config3.locationId;
|
|
1035
|
-
this.userId = config3.userId;
|
|
1036
1132
|
this.registry = config3.registry || null;
|
|
1037
1133
|
}
|
|
1038
1134
|
/**
|
|
@@ -1056,6 +1152,42 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
1056
1152
|
getUserId() {
|
|
1057
1153
|
return this.userId;
|
|
1058
1154
|
}
|
|
1155
|
+
/**
|
|
1156
|
+
* The company whose Firebase auth is currently active. undefined when the
|
|
1157
|
+
* home company id was never configured (GHL_COMPANY_ID / credentials).
|
|
1158
|
+
*/
|
|
1159
|
+
getCurrentCompanyId() {
|
|
1160
|
+
return this.currentCompanyId;
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Swap in a specific company's Firebase auth (multi-tenant). Used by
|
|
1164
|
+
* switch_location so the workflow builder authenticates against the company
|
|
1165
|
+
* that owns the target sub-account. Resets the cached ID token so the next
|
|
1166
|
+
* call mints a fresh one from the new refresh token.
|
|
1167
|
+
*/
|
|
1168
|
+
applyCompanyFirebase(config3) {
|
|
1169
|
+
this.firebaseApiKey = config3.apiKey;
|
|
1170
|
+
this.refreshToken = config3.refreshToken;
|
|
1171
|
+
this.userId = config3.userId;
|
|
1172
|
+
this.currentCompanyId = config3.companyId;
|
|
1173
|
+
this.resetTokenCache();
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Restore the home company's Firebase auth (the install's own credentials).
|
|
1177
|
+
* Called when switching back to a home-company location.
|
|
1178
|
+
*/
|
|
1179
|
+
resetToHomeFirebase() {
|
|
1180
|
+
this.firebaseApiKey = this.homeFirebaseApiKey;
|
|
1181
|
+
this.refreshToken = this.homeRefreshToken;
|
|
1182
|
+
this.userId = this.homeUserId;
|
|
1183
|
+
this.currentCompanyId = this.homeCompanyId;
|
|
1184
|
+
this.resetTokenCache();
|
|
1185
|
+
}
|
|
1186
|
+
resetTokenCache() {
|
|
1187
|
+
this.cachedIdToken = null;
|
|
1188
|
+
this.tokenExpiry = 0;
|
|
1189
|
+
this.tokenRefreshPromise = null;
|
|
1190
|
+
}
|
|
1059
1191
|
/**
|
|
1060
1192
|
* Probe Firebase auth by refreshing the ID token. Used by the health_check
|
|
1061
1193
|
* diagnostic tool. Does not touch GHL backend — just exchanges the refresh
|
|
@@ -1080,6 +1212,7 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
1080
1212
|
const userId = process.env.GHL_USER_ID || firebase?.userId;
|
|
1081
1213
|
const apiKey2 = process.env.GHL_API_KEY;
|
|
1082
1214
|
const locationId2 = process.env.GHL_LOCATION_ID;
|
|
1215
|
+
const companyId = process.env.GHL_COMPANY_ID || void 0;
|
|
1083
1216
|
if (!firebaseApiKey || !refreshToken || !apiKey2 || !locationId2 || !userId) {
|
|
1084
1217
|
return null;
|
|
1085
1218
|
}
|
|
@@ -1089,6 +1222,7 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
1089
1222
|
apiKey: apiKey2,
|
|
1090
1223
|
locationId: locationId2,
|
|
1091
1224
|
userId,
|
|
1225
|
+
companyId,
|
|
1092
1226
|
registry: registry2
|
|
1093
1227
|
});
|
|
1094
1228
|
}
|
|
@@ -1129,13 +1263,32 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
1129
1263
|
this.tokenExpiry = Date.now() + 55 * 60 * 1e3;
|
|
1130
1264
|
if (data.refresh_token && data.refresh_token !== this.refreshToken) {
|
|
1131
1265
|
this.refreshToken = data.refresh_token;
|
|
1132
|
-
this.
|
|
1133
|
-
if (this.registry) {
|
|
1134
|
-
this.registry.updateFirebaseRefreshToken(data.refresh_token);
|
|
1135
|
-
}
|
|
1266
|
+
this.persistRotatedToken(data.refresh_token);
|
|
1136
1267
|
}
|
|
1137
1268
|
return data.id_token;
|
|
1138
1269
|
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Persist a rotated refresh token to the slot that owns the ACTIVE company.
|
|
1272
|
+
*
|
|
1273
|
+
* - A registered client company → its registry slot only (its token lives
|
|
1274
|
+
* in firebaseByCompany, not credentials.json).
|
|
1275
|
+
* - The home company → credentials.json/.env + the home registry slot, and
|
|
1276
|
+
* keep the in-memory home token in sync so resetToHomeFirebase() restores
|
|
1277
|
+
* the fresh value rather than a stale one.
|
|
1278
|
+
*/
|
|
1279
|
+
persistRotatedToken(newToken) {
|
|
1280
|
+
const company = this.currentCompanyId;
|
|
1281
|
+
const isClientCompany = company !== void 0 && company !== this.homeCompanyId && this.registry?.getCompanyFirebase(company) !== void 0;
|
|
1282
|
+
if (isClientCompany) {
|
|
1283
|
+
this.registry?.updateCompanyFirebaseRefreshToken(company, newToken);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (company === void 0 || company === this.homeCompanyId) {
|
|
1287
|
+
this.homeRefreshToken = newToken;
|
|
1288
|
+
}
|
|
1289
|
+
this.persistRefreshToken(newToken);
|
|
1290
|
+
this.registry?.updateFirebaseRefreshToken(newToken);
|
|
1291
|
+
}
|
|
1139
1292
|
/**
|
|
1140
1293
|
* Persist a rotated refresh token to every store that backs it.
|
|
1141
1294
|
*
|
|
@@ -3707,6 +3860,7 @@ function registerBlogTools(server2, client) {
|
|
|
3707
3860
|
|
|
3708
3861
|
// src/tools/emails.ts
|
|
3709
3862
|
var import_zod25 = require("zod");
|
|
3863
|
+
var EMAIL_BUILDER_BASE = "https://backend.leadconnectorhq.com/emails/builder";
|
|
3710
3864
|
var TEMPLATE_TYPES = ["html", "folder", "import", "builder", "blank", "ai_template", "vibe-editor"];
|
|
3711
3865
|
var EDITOR_TYPES = ["html", "builder"];
|
|
3712
3866
|
function registerEmailTools(server2, client) {
|
|
@@ -3777,7 +3931,7 @@ function registerEmailTools(server2, client) {
|
|
|
3777
3931
|
safeTool(
|
|
3778
3932
|
server2,
|
|
3779
3933
|
"update_email_template",
|
|
3780
|
-
"Save HTML content into an existing email template. Use this to update the body of a template after `create_email_template`. The `updatedBy` field is required by GHL; defaults to 'mcp' if not provided. Note: this updates CONTENT only.
|
|
3934
|
+
"Save HTML content into an existing email template. Use this to update the body of a template after `create_email_template`. The `updatedBy` field is required by GHL; defaults to 'mcp' if not provided. Note: this updates CONTENT only. To rename a template use `rename_email_template`; to delete it use `delete_email_template` (both Firebase-gated).",
|
|
3781
3935
|
{
|
|
3782
3936
|
templateId: import_zod25.z.string().describe("The template ID to update (from create_email_template or list_email_templates)."),
|
|
3783
3937
|
html: import_zod25.z.string().describe("The full HTML body of the email. Can include merge fields like {{contact.first_name}}."),
|
|
@@ -3799,6 +3953,78 @@ function registerEmailTools(server2, client) {
|
|
|
3799
3953
|
}
|
|
3800
3954
|
);
|
|
3801
3955
|
}
|
|
3956
|
+
function registerEmailBuilderInternalTools(server2, builderClient) {
|
|
3957
|
+
const client = builderClient;
|
|
3958
|
+
if (!client) return;
|
|
3959
|
+
async function builderRequest(method, path6, body) {
|
|
3960
|
+
const headers = await client.buildHeaders();
|
|
3961
|
+
const response = await fetch(`${EMAIL_BUILDER_BASE}${path6}`, {
|
|
3962
|
+
method,
|
|
3963
|
+
headers,
|
|
3964
|
+
body: body ? JSON.stringify(body) : void 0
|
|
3965
|
+
});
|
|
3966
|
+
if (!response.ok) {
|
|
3967
|
+
const text2 = await response.text();
|
|
3968
|
+
throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path6}
|
|
3969
|
+
${text2}`);
|
|
3970
|
+
}
|
|
3971
|
+
const text = await response.text();
|
|
3972
|
+
return text ? JSON.parse(text) : { ok: true };
|
|
3973
|
+
}
|
|
3974
|
+
server2.tool(
|
|
3975
|
+
"delete_email_template",
|
|
3976
|
+
"Permanently delete an email template ('builder') by id. This is a HARD delete \u2014 the template is removed, not archived (use archive_email_template if you want a reversible remove). Requires Firebase auth; the public bearer API can't do this. Get the templateId from list_email_templates. WARNING: irreversible. If the template is referenced by a workflow email action or a draft campaign, deleting it will break that reference.",
|
|
3977
|
+
{
|
|
3978
|
+
templateId: import_zod25.z.string().describe("The template id to delete (from list_email_templates, the `id` field)."),
|
|
3979
|
+
locationId: import_zod25.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
3980
|
+
},
|
|
3981
|
+
async ({ templateId, locationId: locationId2 }) => {
|
|
3982
|
+
try {
|
|
3983
|
+
const loc = locationId2 ?? client.locationId;
|
|
3984
|
+
const result = await builderRequest("DELETE", `/data/${templateId}?locationId=${loc}`);
|
|
3985
|
+
return jsonResponse(result);
|
|
3986
|
+
} catch (error) {
|
|
3987
|
+
return errorResponse(error);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
);
|
|
3991
|
+
server2.tool(
|
|
3992
|
+
"rename_email_template",
|
|
3993
|
+
"Rename an existing email template ('builder'). Updates the display title only; the HTML content and sender settings are left untouched. Requires Firebase auth; the public bearer API can't do this. Get the templateId from list_email_templates.",
|
|
3994
|
+
{
|
|
3995
|
+
templateId: import_zod25.z.string().describe("The template id to rename (from list_email_templates, the `id` field)."),
|
|
3996
|
+
name: import_zod25.z.string().describe("The new display name for the template (e.g., 'May Newsletter \u2014 Final')."),
|
|
3997
|
+
locationId: import_zod25.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
3998
|
+
},
|
|
3999
|
+
async ({ templateId, name, locationId: locationId2 }) => {
|
|
4000
|
+
try {
|
|
4001
|
+
const loc = locationId2 ?? client.locationId;
|
|
4002
|
+
const result = await builderRequest("PATCH", `/${templateId}`, { locationId: loc, name });
|
|
4003
|
+
return jsonResponse(result);
|
|
4004
|
+
} catch (error) {
|
|
4005
|
+
return errorResponse(error);
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
);
|
|
4009
|
+
server2.tool(
|
|
4010
|
+
"archive_email_template",
|
|
4011
|
+
"Archive (or unarchive) an email template ('builder'). Archiving removes it from the active templates list without deleting it \u2014 a reversible alternative to delete_email_template. Requires Firebase auth. Get the templateId from list_email_templates.",
|
|
4012
|
+
{
|
|
4013
|
+
templateId: import_zod25.z.string().describe("The template id to archive/unarchive (from list_email_templates, the `id` field)."),
|
|
4014
|
+
archived: import_zod25.z.boolean().optional().describe("true to archive (default), false to restore from archive."),
|
|
4015
|
+
locationId: import_zod25.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
4016
|
+
},
|
|
4017
|
+
async ({ templateId, archived, locationId: locationId2 }) => {
|
|
4018
|
+
try {
|
|
4019
|
+
const loc = locationId2 ?? client.locationId;
|
|
4020
|
+
const result = await builderRequest("PATCH", `/${templateId}`, { locationId: loc, archived: archived ?? true });
|
|
4021
|
+
return jsonResponse(result);
|
|
4022
|
+
} catch (error) {
|
|
4023
|
+
return errorResponse(error);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
);
|
|
4027
|
+
}
|
|
3802
4028
|
|
|
3803
4029
|
// src/tools/trigger-links.ts
|
|
3804
4030
|
var import_zod26 = require("zod");
|
|
@@ -5431,99 +5657,347 @@ ${text2}`);
|
|
|
5431
5657
|
}
|
|
5432
5658
|
|
|
5433
5659
|
// src/tools/location-switcher.ts
|
|
5660
|
+
var import_zod38 = require("zod");
|
|
5661
|
+
|
|
5662
|
+
// src/setup-tool.ts
|
|
5663
|
+
var os2 = __toESM(require("os"));
|
|
5664
|
+
var crypto2 = __toESM(require("crypto"));
|
|
5434
5665
|
var import_zod37 = require("zod");
|
|
5435
|
-
var
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5666
|
+
var LICENSE_API = "https://elitedcs.com/api/validate-license";
|
|
5667
|
+
var GHL_API = "https://services.leadconnectorhq.com";
|
|
5668
|
+
var FIREBASE_TOKEN_API = "https://securetoken.googleapis.com/v1/token";
|
|
5669
|
+
function deviceFingerprint() {
|
|
5670
|
+
const raw = `${os2.hostname()}:${os2.userInfo().username}:${os2.platform()}:${os2.arch()}`;
|
|
5671
|
+
return crypto2.createHash("sha256").update(raw).digest("hex").slice(0, 16);
|
|
5440
5672
|
}
|
|
5441
|
-
function
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
content: [
|
|
5457
|
-
{
|
|
5458
|
-
type: "text",
|
|
5459
|
-
text: `Current location: ${loc?.name || "Unknown"}
|
|
5460
|
-
ID: ${client.defaultLocationId}
|
|
5461
|
-
API Key: ${client.getApiKeyPrefix()}
|
|
5462
|
-
Address: ${loc?.address || "N/A"}
|
|
5463
|
-
Email: ${loc?.email || "N/A"}
|
|
5464
|
-
Token registry: ${registeredCount} location(s) registered${versionLine}`
|
|
5465
|
-
}
|
|
5466
|
-
]
|
|
5467
|
-
};
|
|
5468
|
-
}
|
|
5469
|
-
return {
|
|
5470
|
-
content: [{ type: "text", text: `No default location set. Pass locationId to tools or use switch_location.${versionLine}` }]
|
|
5471
|
-
};
|
|
5472
|
-
} catch (error) {
|
|
5473
|
-
return {
|
|
5474
|
-
content: [{ type: "text", text: `Current location ID: ${locId} (could not fetch details \u2014 API key may not have access)${versionLine}` }]
|
|
5475
|
-
};
|
|
5476
|
-
}
|
|
5673
|
+
async function validateLicense(email, licenseKey) {
|
|
5674
|
+
try {
|
|
5675
|
+
const res = await fetch(LICENSE_API, {
|
|
5676
|
+
method: "POST",
|
|
5677
|
+
headers: { "Content-Type": "application/json" },
|
|
5678
|
+
body: JSON.stringify({
|
|
5679
|
+
email: email.trim(),
|
|
5680
|
+
license_key: licenseKey.trim(),
|
|
5681
|
+
device_fingerprint: deviceFingerprint()
|
|
5682
|
+
}),
|
|
5683
|
+
signal: AbortSignal.timeout(1e4)
|
|
5684
|
+
});
|
|
5685
|
+
const data = await res.json().catch(() => ({}));
|
|
5686
|
+
if (res.ok && data.valid) {
|
|
5687
|
+
return { ok: true, installs: `${data.installs_used}/${data.installs_max}` };
|
|
5477
5688
|
}
|
|
5478
|
-
|
|
5689
|
+
return { ok: false, error: data.error || data.message || `License validation failed (HTTP ${res.status})` };
|
|
5690
|
+
} catch (err) {
|
|
5691
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5692
|
+
return { ok: false, error: `Could not reach license server: ${msg}` };
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
async function validateGhl(apiKey2, locationId2) {
|
|
5696
|
+
try {
|
|
5697
|
+
const res = await fetch(`${GHL_API}/locations/${locationId2}`, {
|
|
5698
|
+
headers: {
|
|
5699
|
+
Authorization: `Bearer ${apiKey2}`,
|
|
5700
|
+
Version: "2021-07-28",
|
|
5701
|
+
Accept: "application/json"
|
|
5702
|
+
},
|
|
5703
|
+
signal: AbortSignal.timeout(1e4)
|
|
5704
|
+
});
|
|
5705
|
+
if (res.status === 401) return { ok: false, error: "GHL API key is invalid (401)." };
|
|
5706
|
+
if (res.status === 403) return { ok: false, error: "GHL API key doesn't have access to this Location ID. The key must be created INSIDE this sub-account." };
|
|
5707
|
+
if (!res.ok) return { ok: false, error: `GHL returned HTTP ${res.status}.` };
|
|
5708
|
+
const data = await res.json().catch(() => ({}));
|
|
5709
|
+
const name = data?.location?.name || data?.name || "Unknown";
|
|
5710
|
+
return { ok: true, locationName: name };
|
|
5711
|
+
} catch (err) {
|
|
5712
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5713
|
+
return { ok: false, error: `Could not reach GHL: ${msg}` };
|
|
5714
|
+
}
|
|
5715
|
+
}
|
|
5716
|
+
async function validateFirebase(firebaseKey, refreshToken) {
|
|
5717
|
+
try {
|
|
5718
|
+
const res = await fetch(`${FIREBASE_TOKEN_API}?key=${encodeURIComponent(firebaseKey)}`, {
|
|
5719
|
+
method: "POST",
|
|
5720
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
5721
|
+
body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,
|
|
5722
|
+
signal: AbortSignal.timeout(1e4)
|
|
5723
|
+
});
|
|
5724
|
+
if (!res.ok) return { ok: false, error: "Firebase credentials rejected. Re-extract them from your GHL browser tab." };
|
|
5725
|
+
return { ok: true };
|
|
5726
|
+
} catch (err) {
|
|
5727
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5728
|
+
return { ok: false, error: `Could not reach Firebase: ${msg}` };
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
function registerSetupTool(server2) {
|
|
5479
5732
|
server2.tool(
|
|
5480
|
-
"
|
|
5481
|
-
"
|
|
5733
|
+
"setup_ghl_mcp",
|
|
5734
|
+
"First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 206 tools (163 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
|
|
5482
5735
|
{
|
|
5483
|
-
|
|
5736
|
+
email: import_zod37.z.string().email().describe("Email used at purchase."),
|
|
5737
|
+
license_key: import_zod37.z.string().min(20).describe("License key from your purchase email."),
|
|
5738
|
+
ghl_api_key: import_zod37.z.string().min(10).describe("GHL Private Integration key (starts with 'pit-'). Created INSIDE the sub-account at Settings > Integrations > Private Integrations."),
|
|
5739
|
+
ghl_location_id: import_zod37.z.string().min(10).describe("GHL Location ID (sub-account ID). Found in your GHL URL: /location/THIS_PART/dashboard."),
|
|
5740
|
+
ghl_company_id: import_zod37.z.string().optional().describe("(Agency only) Company ID for multi-location access."),
|
|
5741
|
+
ghl_user_id: import_zod37.z.string().optional().describe("(Workflow Builder, optional) Firebase User ID. See README for browser capture instructions."),
|
|
5742
|
+
ghl_firebase_api_key: import_zod37.z.string().optional().describe("(Workflow Builder, optional) Firebase API Key starting with 'AIza'."),
|
|
5743
|
+
ghl_firebase_refresh_token: import_zod37.z.string().optional().describe("(Workflow Builder, optional) Firebase refresh token starting with 'AMf-'.")
|
|
5484
5744
|
},
|
|
5485
|
-
async (
|
|
5486
|
-
const
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
const previousBuilderApiKey = builderClient?.getApiKey();
|
|
5490
|
-
try {
|
|
5491
|
-
let keySwapped = false;
|
|
5492
|
-
if (registry2) {
|
|
5493
|
-
const token = registry2.getToken(locationId2);
|
|
5494
|
-
if (token) {
|
|
5495
|
-
client.setApiKey(token.apiKey);
|
|
5496
|
-
if (builderClient) {
|
|
5497
|
-
builderClient.setApiKey(token.apiKey);
|
|
5498
|
-
}
|
|
5499
|
-
keySwapped = true;
|
|
5500
|
-
}
|
|
5501
|
-
}
|
|
5502
|
-
const result = await client.get(`/locations/${locationId2}`);
|
|
5503
|
-
const loc = result.location ?? result;
|
|
5504
|
-
const name = loc?.name || "Unknown";
|
|
5505
|
-
client.defaultLocationId = locationId2;
|
|
5506
|
-
if (builderClient) {
|
|
5507
|
-
builderClient.locationId = locationId2;
|
|
5508
|
-
}
|
|
5509
|
-
const keyStatus = keySwapped ? `API key swapped: ${previousKeyPrefix} \u2192 ${client.getApiKeyPrefix()}` : `API key unchanged (${client.getApiKeyPrefix()})${!registry2?.getToken(locationId2) ? " \u2014 consider using register_location to add this location's key" : ""}`;
|
|
5510
|
-
return {
|
|
5511
|
-
content: [
|
|
5512
|
-
{
|
|
5513
|
-
type: "text",
|
|
5514
|
-
text: `Switched to: ${name} (${locationId2})
|
|
5515
|
-
Previous: ${previousId || "none"}
|
|
5516
|
-
${keyStatus}
|
|
5745
|
+
async (args) => {
|
|
5746
|
+
const lic = await validateLicense(args.email, args.license_key);
|
|
5747
|
+
if (!lic.ok) {
|
|
5748
|
+
return { content: [{ type: "text", text: `License check failed: ${lic.error}
|
|
5517
5749
|
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5750
|
+
Purchase a license at https://elitedcs.com/ghl-mcp-server or contact support.` }], isError: true };
|
|
5751
|
+
}
|
|
5752
|
+
const ghl = await validateGhl(args.ghl_api_key, args.ghl_location_id);
|
|
5753
|
+
if (!ghl.ok) {
|
|
5754
|
+
return { content: [{ type: "text", text: `GHL credential check failed: ${ghl.error}` }], isError: true };
|
|
5755
|
+
}
|
|
5756
|
+
const wantsWorkflowBuilder = args.ghl_user_id || args.ghl_firebase_api_key || args.ghl_firebase_refresh_token;
|
|
5757
|
+
let workflowBuilderEnabled = false;
|
|
5758
|
+
let workflowBuilderNote = "";
|
|
5759
|
+
if (wantsWorkflowBuilder) {
|
|
5760
|
+
if (!args.ghl_user_id || !args.ghl_firebase_api_key || !args.ghl_firebase_refresh_token) {
|
|
5761
|
+
return {
|
|
5762
|
+
content: [{
|
|
5763
|
+
type: "text",
|
|
5764
|
+
text: "Workflow Builder requires ALL THREE Firebase fields: ghl_user_id, ghl_firebase_api_key, ghl_firebase_refresh_token. Either provide all three or omit all three. See https://elitedcs.com/ghl-mcp-server/firebase for one-click capture."
|
|
5765
|
+
}],
|
|
5766
|
+
isError: true
|
|
5767
|
+
};
|
|
5768
|
+
}
|
|
5769
|
+
const fb = await validateFirebase(args.ghl_firebase_api_key, args.ghl_firebase_refresh_token);
|
|
5770
|
+
if (!fb.ok) {
|
|
5771
|
+
workflowBuilderNote = `
|
|
5772
|
+
|
|
5773
|
+
Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builder. Re-run setup_ghl_mcp later with fresh Firebase fields to enable it.`;
|
|
5774
|
+
} else {
|
|
5775
|
+
workflowBuilderEnabled = true;
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5778
|
+
writeCredentials({
|
|
5779
|
+
license_key: args.license_key.trim(),
|
|
5780
|
+
email: args.email.trim(),
|
|
5781
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5782
|
+
ghl_api_key: args.ghl_api_key.trim(),
|
|
5783
|
+
ghl_location_id: args.ghl_location_id.trim(),
|
|
5784
|
+
ghl_company_id: args.ghl_company_id?.trim() || void 0,
|
|
5785
|
+
ghl_user_id: workflowBuilderEnabled ? args.ghl_user_id?.trim() : void 0,
|
|
5786
|
+
ghl_firebase_api_key: workflowBuilderEnabled ? args.ghl_firebase_api_key?.trim() : void 0,
|
|
5787
|
+
ghl_firebase_refresh_token: workflowBuilderEnabled ? args.ghl_firebase_refresh_token?.trim() : void 0
|
|
5788
|
+
});
|
|
5789
|
+
const toolCount = workflowBuilderEnabled ? "206" : "163";
|
|
5790
|
+
const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
|
|
5791
|
+
const wfTip = workflowBuilderEnabled ? "" : "\nTo enable Workflow Builder later (8 extra tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
|
|
5792
|
+
return {
|
|
5793
|
+
content: [{
|
|
5794
|
+
type: "text",
|
|
5795
|
+
text: [
|
|
5796
|
+
`Setup complete!`,
|
|
5797
|
+
``,
|
|
5798
|
+
`License: verified (installs ${lic.installs}).`,
|
|
5799
|
+
`GHL: connected to "${ghl.locationName}".`,
|
|
5800
|
+
wfLine,
|
|
5801
|
+
``,
|
|
5802
|
+
`Credentials saved to: ${credentialsPath()}`,
|
|
5803
|
+
``,
|
|
5804
|
+
`**Restart Claude (quit fully and reopen) to load all ${toolCount} tools.**`,
|
|
5805
|
+
``,
|
|
5806
|
+
`After restart, try: "List my GHL contacts" or "Show my pipelines".${wfTip}`,
|
|
5807
|
+
workflowBuilderNote
|
|
5808
|
+
].join("\n")
|
|
5809
|
+
}]
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
);
|
|
5813
|
+
}
|
|
5814
|
+
function registerEnableWorkflowBuilderTool(server2) {
|
|
5815
|
+
server2.tool(
|
|
5816
|
+
"enable_workflow_builder",
|
|
5817
|
+
"Add Firebase credentials to an existing GHL Command install to unlock 30 additional tools across 6 modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder (10 tools), form builder (5 tools), pipeline builder (5 tools), and workflow cloning. Requires you've already run setup_ghl_mcp. Capture the three Firebase values from your GHL browser session \u2014 see elitedcs.com/ghl-mcp-firebase for step-by-step DevTools instructions. Tool count goes from 163 to 203 after the next Claude restart.",
|
|
5818
|
+
{
|
|
5819
|
+
ghl_user_id: import_zod37.z.string().min(10).describe("Firebase User ID (uid). DevTools \u2192 Application \u2192 IndexedDB \u2192 firebaseLocalStorageDb \u2192 firebaseLocalStorage \u2192 the value.uid field of the firebase:authUser row."),
|
|
5820
|
+
ghl_firebase_api_key: import_zod37.z.string().min(10).describe("Firebase API Key starting with 'AIza'. The string between 'firebase:authUser:' and ':[DEFAULT]' in the row's Key column."),
|
|
5821
|
+
ghl_firebase_refresh_token: import_zod37.z.string().min(10).describe("Firebase refresh token. value.stsTokenManager.refreshToken in the firebase:authUser row.")
|
|
5822
|
+
},
|
|
5823
|
+
async (args) => {
|
|
5824
|
+
const existing = readCredentials();
|
|
5825
|
+
if (!existing) {
|
|
5826
|
+
return {
|
|
5827
|
+
content: [{
|
|
5828
|
+
type: "text",
|
|
5829
|
+
text: "No existing credentials found at " + credentialsPath() + ".\n\nRun setup_ghl_mcp first to register your license and basic GHL credentials, then come back to this tool to add Workflow Builder."
|
|
5830
|
+
}],
|
|
5831
|
+
isError: true
|
|
5832
|
+
};
|
|
5833
|
+
}
|
|
5834
|
+
const fb = await validateFirebase(args.ghl_firebase_api_key.trim(), args.ghl_firebase_refresh_token.trim());
|
|
5835
|
+
if (!fb.ok) {
|
|
5836
|
+
return {
|
|
5837
|
+
content: [{
|
|
5838
|
+
type: "text",
|
|
5839
|
+
text: `Firebase credentials rejected: ${fb.error}
|
|
5840
|
+
|
|
5841
|
+
Common causes:
|
|
5842
|
+
- The refresh token has rotated (they rotate every few weeks). Re-extract from your GHL browser tab.
|
|
5843
|
+
- The Firebase API Key doesn't match the refresh token's project. Both must come from the SAME firebase:authUser row.
|
|
5844
|
+
|
|
5845
|
+
DevTools steps: https://elitedcs.com/ghl-mcp-firebase`
|
|
5846
|
+
}],
|
|
5847
|
+
isError: true
|
|
5848
|
+
};
|
|
5849
|
+
}
|
|
5850
|
+
writeCredentials({
|
|
5851
|
+
...existing,
|
|
5852
|
+
ghl_user_id: args.ghl_user_id.trim(),
|
|
5853
|
+
ghl_firebase_api_key: args.ghl_firebase_api_key.trim(),
|
|
5854
|
+
ghl_firebase_refresh_token: args.ghl_firebase_refresh_token.trim()
|
|
5855
|
+
});
|
|
5856
|
+
return {
|
|
5857
|
+
content: [{
|
|
5858
|
+
type: "text",
|
|
5859
|
+
text: [
|
|
5860
|
+
"Workflow Builder enabled!",
|
|
5861
|
+
"",
|
|
5862
|
+
"Firebase credentials verified and saved.",
|
|
5863
|
+
"",
|
|
5864
|
+
"**Restart Claude (quit fully and reopen) to load the workflow builder + funnel builder + pipeline builder + form builder + workflow cloner tools (203 total).**",
|
|
5865
|
+
"",
|
|
5866
|
+
'After restart, try: "List my workflows in full detail" or "Validate workflow <id>".',
|
|
5867
|
+
"",
|
|
5868
|
+
"Note: Firebase refresh tokens rotate every few weeks. If workflow tools stop working, re-run enable_workflow_builder with fresh values from a current GHL browser session."
|
|
5869
|
+
].join("\n")
|
|
5870
|
+
}]
|
|
5871
|
+
};
|
|
5872
|
+
}
|
|
5873
|
+
);
|
|
5874
|
+
}
|
|
5875
|
+
|
|
5876
|
+
// src/tools/location-switcher.ts
|
|
5877
|
+
function routeFirebaseForCompany(builderClient, registry2, companyId) {
|
|
5878
|
+
if (!builderClient) return "";
|
|
5879
|
+
if (!companyId) {
|
|
5880
|
+
return "\nFirebase: could not determine this location's company \u2014 workflow-builder auth left unchanged.";
|
|
5881
|
+
}
|
|
5882
|
+
const cfg = registry2?.getCompanyFirebase(companyId);
|
|
5883
|
+
if (cfg) {
|
|
5884
|
+
builderClient.applyCompanyFirebase({
|
|
5885
|
+
apiKey: cfg.apiKey,
|
|
5886
|
+
refreshToken: cfg.refreshToken,
|
|
5887
|
+
userId: cfg.userId,
|
|
5888
|
+
companyId
|
|
5889
|
+
});
|
|
5890
|
+
return `
|
|
5891
|
+
Firebase: using ${cfg.name || `company ${companyId}`} credentials \u2014 workflow builder enabled here.`;
|
|
5892
|
+
}
|
|
5893
|
+
builderClient.resetToHomeFirebase();
|
|
5894
|
+
const homeCompanyId = builderClient.getCurrentCompanyId();
|
|
5895
|
+
if (homeCompanyId === void 0) {
|
|
5896
|
+
return "\nFirebase: using home credentials. Home company id isn't configured (set GHL_COMPANY_ID), so if this is a client account its workflow-builder tools will 401 until you run register_company_firebase.";
|
|
5897
|
+
}
|
|
5898
|
+
if (homeCompanyId === companyId) {
|
|
5899
|
+
return "\nFirebase: home company \u2014 workflow builder enabled.";
|
|
5900
|
+
}
|
|
5901
|
+
return `
|
|
5902
|
+
Firebase: NOT configured for company ${companyId}. Workflow builder, funnels, forms, pipelines, smart lists, reputation, email campaigns and memberships will fail here (401) until you run register_company_firebase for this company. Public-API tools (contacts, opportunities, calendars, etc.) still work.`;
|
|
5903
|
+
}
|
|
5904
|
+
var switchChain = Promise.resolve();
|
|
5905
|
+
function withSwitchLock(fn) {
|
|
5906
|
+
const next = switchChain.then(fn, fn);
|
|
5907
|
+
switchChain = next.catch(() => void 0);
|
|
5908
|
+
return next;
|
|
5909
|
+
}
|
|
5910
|
+
function registerLocationSwitcherTools(server2, client, builderClient, registry2, mcpVersion) {
|
|
5911
|
+
const versionLine = mcpVersion ? `
|
|
5912
|
+
MCP version: v${mcpVersion}` : "";
|
|
5913
|
+
server2.tool(
|
|
5914
|
+
"get_current_location",
|
|
5915
|
+
"Show which GHL sub-account (location) is currently active, including API key status and installed MCP version. All tools use this location by default unless you specify a different one.",
|
|
5916
|
+
{},
|
|
5917
|
+
async () => {
|
|
5918
|
+
const locId = client.defaultLocationId || "NOT SET";
|
|
5919
|
+
try {
|
|
5920
|
+
if (client.defaultLocationId) {
|
|
5921
|
+
const result = await client.get(`/locations/${client.defaultLocationId}`);
|
|
5922
|
+
const loc = result.location ?? result;
|
|
5923
|
+
const registeredCount = registry2 ? registry2.listLocations().length : 0;
|
|
5924
|
+
return {
|
|
5925
|
+
content: [
|
|
5926
|
+
{
|
|
5927
|
+
type: "text",
|
|
5928
|
+
text: `Current location: ${loc?.name || "Unknown"}
|
|
5929
|
+
ID: ${client.defaultLocationId}
|
|
5930
|
+
API Key: ${client.getApiKeyPrefix()}
|
|
5931
|
+
Address: ${loc?.address || "N/A"}
|
|
5932
|
+
Email: ${loc?.email || "N/A"}
|
|
5933
|
+
Token registry: ${registeredCount} location(s) registered${versionLine}`
|
|
5934
|
+
}
|
|
5935
|
+
]
|
|
5936
|
+
};
|
|
5937
|
+
}
|
|
5938
|
+
return {
|
|
5939
|
+
content: [{ type: "text", text: `No default location set. Pass locationId to tools or use switch_location.${versionLine}` }]
|
|
5940
|
+
};
|
|
5941
|
+
} catch (error) {
|
|
5942
|
+
return {
|
|
5943
|
+
content: [{ type: "text", text: `Current location ID: ${locId} (could not fetch details \u2014 API key may not have access)${versionLine}` }]
|
|
5944
|
+
};
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
);
|
|
5948
|
+
server2.tool(
|
|
5949
|
+
"switch_location",
|
|
5950
|
+
"Switch the active GHL sub-account. Automatically swaps the API key from the token registry if available. After switching, all tools default to the new location.",
|
|
5951
|
+
{
|
|
5952
|
+
locationId: import_zod38.z.string().describe("The Location ID to switch to.")
|
|
5953
|
+
},
|
|
5954
|
+
async ({ locationId: locationId2 }) => withSwitchLock(async () => {
|
|
5955
|
+
const previousId = client.defaultLocationId;
|
|
5956
|
+
const previousKeyPrefix = client.getApiKeyPrefix();
|
|
5957
|
+
const previousApiKey = client.getApiKey();
|
|
5958
|
+
const previousBuilderApiKey = builderClient?.getApiKey();
|
|
5959
|
+
try {
|
|
5960
|
+
let keySwapped = false;
|
|
5961
|
+
if (registry2) {
|
|
5962
|
+
const token = registry2.getToken(locationId2);
|
|
5963
|
+
if (token) {
|
|
5964
|
+
client.setApiKey(token.apiKey);
|
|
5965
|
+
if (builderClient) {
|
|
5966
|
+
builderClient.setApiKey(token.apiKey);
|
|
5967
|
+
}
|
|
5968
|
+
keySwapped = true;
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
const result = await client.get(`/locations/${locationId2}`);
|
|
5972
|
+
const loc = result.location ?? result;
|
|
5973
|
+
const name = loc?.name || "Unknown";
|
|
5974
|
+
client.defaultLocationId = locationId2;
|
|
5975
|
+
if (builderClient) {
|
|
5976
|
+
builderClient.locationId = locationId2;
|
|
5977
|
+
}
|
|
5978
|
+
const targetCompanyId = loc?.companyId || registry2?.getToken(locationId2)?.companyId;
|
|
5979
|
+
if (targetCompanyId && registry2?.getToken(locationId2)) {
|
|
5980
|
+
registry2.setLocationCompanyId(locationId2, targetCompanyId);
|
|
5981
|
+
}
|
|
5982
|
+
const firebaseStatus = routeFirebaseForCompany(builderClient, registry2, targetCompanyId);
|
|
5983
|
+
const keyStatus = keySwapped ? `API key swapped: ${previousKeyPrefix} \u2192 ${client.getApiKeyPrefix()}` : `API key unchanged (${client.getApiKeyPrefix()})${!registry2?.getToken(locationId2) ? " \u2014 consider using register_location to add this location's key" : ""}`;
|
|
5984
|
+
return {
|
|
5985
|
+
content: [
|
|
5986
|
+
{
|
|
5987
|
+
type: "text",
|
|
5988
|
+
text: `Switched to: ${name} (${locationId2})
|
|
5989
|
+
Previous: ${previousId || "none"}
|
|
5990
|
+
${keyStatus}${firebaseStatus}
|
|
5991
|
+
|
|
5992
|
+
All tools now default to this location.`
|
|
5993
|
+
}
|
|
5994
|
+
]
|
|
5995
|
+
};
|
|
5996
|
+
} catch (error) {
|
|
5997
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5998
|
+
client.defaultLocationId = previousId;
|
|
5999
|
+
client.setApiKey(previousApiKey);
|
|
6000
|
+
if (builderClient && previousBuilderApiKey !== void 0) {
|
|
5527
6001
|
builderClient.setApiKey(previousBuilderApiKey);
|
|
5528
6002
|
}
|
|
5529
6003
|
if (builderClient && previousId !== void 0) {
|
|
@@ -5545,9 +6019,9 @@ Still on: ${previousId || "none"}${hint}` }],
|
|
|
5545
6019
|
"register_location",
|
|
5546
6020
|
"Add a GHL sub-account to the token registry so switch_location can automatically use its API key. Each sub-account needs its own Private Integration key created in GHL Settings > Integrations.",
|
|
5547
6021
|
{
|
|
5548
|
-
locationId:
|
|
5549
|
-
name:
|
|
5550
|
-
apiKey:
|
|
6022
|
+
locationId: import_zod38.z.string().describe("The GHL Location ID (from Settings > Business Profile)."),
|
|
6023
|
+
name: import_zod38.z.string().describe("A friendly name for this sub-account (e.g. 'PNTracker', 'Med Spa Template')."),
|
|
6024
|
+
apiKey: import_zod38.z.string().describe("The Private Integration API key for this sub-account (starts with 'pit-').")
|
|
5551
6025
|
},
|
|
5552
6026
|
async ({ locationId: locationId2, name, apiKey: apiKey2 }) => {
|
|
5553
6027
|
if (!registry2) {
|
|
@@ -5561,14 +6035,21 @@ Still on: ${previousId || "none"}${hint}` }],
|
|
|
5561
6035
|
const result = await testClient.get(`/locations/${locationId2}`);
|
|
5562
6036
|
const loc = result.location ?? result;
|
|
5563
6037
|
const confirmedName = loc?.name || name;
|
|
5564
|
-
|
|
6038
|
+
const companyId = loc?.companyId;
|
|
6039
|
+
registry2.registerLocation(locationId2, confirmedName, apiKey2, companyId);
|
|
6040
|
+
let fbLine = "";
|
|
6041
|
+
if (companyId) {
|
|
6042
|
+
fbLine = registry2.getCompanyFirebase(companyId) ? `
|
|
6043
|
+
Company: ${companyId} (Firebase already registered \u2014 workflow builder will work here).` : `
|
|
6044
|
+
Company: ${companyId}. To use the workflow builder in this account, run register_company_firebase for this company (Firebase is company-scoped).`;
|
|
6045
|
+
}
|
|
5565
6046
|
return {
|
|
5566
6047
|
content: [
|
|
5567
6048
|
{
|
|
5568
6049
|
type: "text",
|
|
5569
6050
|
text: `Registered: ${confirmedName} (${locationId2})
|
|
5570
6051
|
API Key: ${apiKey2.substring(0, 12)}...
|
|
5571
|
-
Saved to
|
|
6052
|
+
Saved to the token registry.${fbLine}
|
|
5572
6053
|
|
|
5573
6054
|
You can now use switch_location to switch to this sub-account.`
|
|
5574
6055
|
}
|
|
@@ -5597,7 +6078,7 @@ The API key could not access location ${locationId2}. Make sure:
|
|
|
5597
6078
|
"unregister_location",
|
|
5598
6079
|
"Remove a GHL sub-account from the token registry.",
|
|
5599
6080
|
{
|
|
5600
|
-
locationId:
|
|
6081
|
+
locationId: import_zod38.z.string().describe("The Location ID to remove.")
|
|
5601
6082
|
},
|
|
5602
6083
|
async ({ locationId: locationId2 }) => {
|
|
5603
6084
|
if (!registry2) {
|
|
@@ -5619,34 +6100,146 @@ The API key could not access location ${locationId2}. Make sure:
|
|
|
5619
6100
|
};
|
|
5620
6101
|
}
|
|
5621
6102
|
);
|
|
6103
|
+
server2.tool(
|
|
6104
|
+
"register_company_firebase",
|
|
6105
|
+
"Register a GHL company's Firebase credentials so the workflow builder and all Firebase-gated tools work when you switch into THAT company's sub-accounts. Firebase refresh tokens are company-scoped, so managing a client's GHL (e.g. an account where you're an admin user) requires that company's own token. Capture the values from a browser session logged into the client's account and register them keyed by the client's companyId. After this, switch_location to any of that company's locations authenticates the workflow builder correctly. DevTools capture steps: elitedcs.com/ghl-mcp-firebase.",
|
|
6106
|
+
{
|
|
6107
|
+
companyId: import_zod38.z.string().describe("The GHL company/agency ID that owns the client's sub-accounts. Surfaced in switch_location and register_location output, or the GHL URL when viewing the agency."),
|
|
6108
|
+
name: import_zod38.z.string().describe("Friendly name for this client/company (e.g. 'Nathan \u2014 Acme Health')."),
|
|
6109
|
+
ghl_firebase_refresh_token: import_zod38.z.string().min(10).describe("Firebase refresh token captured from a browser session logged into THIS company's GHL. value.stsTokenManager.refreshToken in the firebase:authUser IndexedDB row."),
|
|
6110
|
+
ghl_user_id: import_zod38.z.string().min(5).describe("Firebase User ID (uid) from the same session. value.uid in the firebase:authUser row."),
|
|
6111
|
+
ghl_firebase_api_key: import_zod38.z.string().optional().describe("Firebase API key (starts with 'AIza'). Optional \u2014 defaults to your home Firebase API key, which is identical across GHL accounts."),
|
|
6112
|
+
test_location_id: import_zod38.z.string().optional().describe("Optional but recommended: a registered location ID belonging to this company. The tool then makes a real workflow-builder call to confirm these credentials actually work for this company before you rely on them.")
|
|
6113
|
+
},
|
|
6114
|
+
async (args) => {
|
|
6115
|
+
if (!registry2) {
|
|
6116
|
+
return {
|
|
6117
|
+
content: [{ type: "text", text: "Token registry not available." }],
|
|
6118
|
+
isError: true
|
|
6119
|
+
};
|
|
6120
|
+
}
|
|
6121
|
+
const apiKey2 = args.ghl_firebase_api_key?.trim() || process.env.GHL_FIREBASE_API_KEY;
|
|
6122
|
+
if (!apiKey2) {
|
|
6123
|
+
return {
|
|
6124
|
+
content: [{ type: "text", text: "No Firebase API key available. Pass ghl_firebase_api_key, or configure your home Firebase first via setup_ghl_mcp / enable_workflow_builder (the key is the same across GHL accounts)." }],
|
|
6125
|
+
isError: true
|
|
6126
|
+
};
|
|
6127
|
+
}
|
|
6128
|
+
const refreshToken = args.ghl_firebase_refresh_token.trim();
|
|
6129
|
+
const userId = args.ghl_user_id.trim();
|
|
6130
|
+
const fb = await validateFirebase(apiKey2, refreshToken);
|
|
6131
|
+
if (!fb.ok) {
|
|
6132
|
+
return {
|
|
6133
|
+
content: [{ type: "text", text: `Firebase credentials rejected: ${fb.error}
|
|
6134
|
+
|
|
6135
|
+
Capture fresh values from a browser session logged into THIS company's GHL. Steps: https://elitedcs.com/ghl-mcp-firebase` }],
|
|
6136
|
+
isError: true
|
|
6137
|
+
};
|
|
6138
|
+
}
|
|
6139
|
+
registry2.setCompanyFirebase(args.companyId, { apiKey: apiKey2, refreshToken, userId, name: args.name });
|
|
6140
|
+
let testLine = "";
|
|
6141
|
+
if (args.test_location_id) {
|
|
6142
|
+
const token = registry2.getToken(args.test_location_id);
|
|
6143
|
+
if (!token) {
|
|
6144
|
+
testLine = `
|
|
6145
|
+
|
|
6146
|
+
Skipped end-to-end test: location ${args.test_location_id} isn't registered. Run register_location for it, then switch_location to confirm.`;
|
|
6147
|
+
} else {
|
|
6148
|
+
const probe = new WorkflowBuilderClient({
|
|
6149
|
+
firebaseApiKey: apiKey2,
|
|
6150
|
+
refreshToken,
|
|
6151
|
+
apiKey: token.apiKey,
|
|
6152
|
+
locationId: args.test_location_id,
|
|
6153
|
+
userId,
|
|
6154
|
+
companyId: args.companyId,
|
|
6155
|
+
registry: null
|
|
6156
|
+
});
|
|
6157
|
+
try {
|
|
6158
|
+
await probe.listWorkflows(1, 0);
|
|
6159
|
+
testLine = `
|
|
6160
|
+
|
|
6161
|
+
Verified: the workflow-builder API responded for "${token.name}" (${args.test_location_id}). These credentials work for this company.`;
|
|
6162
|
+
} catch (error) {
|
|
6163
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6164
|
+
testLine = `
|
|
6165
|
+
|
|
6166
|
+
WARNING: saved, but the end-to-end test FAILED for ${args.test_location_id}:
|
|
6167
|
+
${message}
|
|
6168
|
+
|
|
6169
|
+
Likely causes: the refresh token was captured from a DIFFERENT company's session, or that location's PIT key lacks access. Re-capture from a session inside THIS company's GHL.`;
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
}
|
|
6173
|
+
return {
|
|
6174
|
+
content: [{ type: "text", text: `Registered Firebase for ${args.name} (company ${args.companyId}).${testLine}
|
|
6175
|
+
|
|
6176
|
+
Now run switch_location to any of this company's sub-accounts \u2014 the workflow builder will authenticate against it automatically.` }]
|
|
6177
|
+
};
|
|
6178
|
+
}
|
|
6179
|
+
);
|
|
6180
|
+
server2.tool(
|
|
6181
|
+
"unregister_company_firebase",
|
|
6182
|
+
"Remove a company's Firebase credentials from the registry. Workflow-builder tools will stop working for that company's sub-accounts until re-registered.",
|
|
6183
|
+
{
|
|
6184
|
+
companyId: import_zod38.z.string().describe("The company ID to remove Firebase credentials for.")
|
|
6185
|
+
},
|
|
6186
|
+
async ({ companyId }) => {
|
|
6187
|
+
if (!registry2) {
|
|
6188
|
+
return {
|
|
6189
|
+
content: [{ type: "text", text: "Token registry not available." }],
|
|
6190
|
+
isError: true
|
|
6191
|
+
};
|
|
6192
|
+
}
|
|
6193
|
+
const removed = registry2.removeCompanyFirebase(companyId);
|
|
6194
|
+
return {
|
|
6195
|
+
content: [{ type: "text", text: removed ? `Removed Firebase credentials for company ${companyId}.` : `No Firebase credentials were registered for company ${companyId}.` }],
|
|
6196
|
+
isError: !removed
|
|
6197
|
+
};
|
|
6198
|
+
}
|
|
6199
|
+
);
|
|
5622
6200
|
server2.tool(
|
|
5623
6201
|
"list_registered_locations",
|
|
5624
|
-
"List all GHL sub-accounts in the token registry with
|
|
6202
|
+
"List all GHL sub-accounts in the token registry (names, IDs, owning company) plus any companies with registered Firebase credentials for multi-tenant workflow-builder access. These are locations switch_location can automatically authenticate to.",
|
|
5625
6203
|
{},
|
|
5626
6204
|
async () => {
|
|
6205
|
+
const companies = registry2?.listCompanyFirebases() ?? [];
|
|
6206
|
+
const companiesWithFirebase = new Set(companies.map((c) => c.companyId));
|
|
5627
6207
|
if (!registry2 || !registry2.hasTokens()) {
|
|
6208
|
+
const fbSection2 = companies.length ? `
|
|
6209
|
+
|
|
6210
|
+
Company Firebase credentials (${companies.length}):
|
|
6211
|
+
${companies.map((c) => ` ${c.name || c.companyId} (${c.companyId})`).join("\n")}` : "";
|
|
5628
6212
|
return {
|
|
5629
6213
|
content: [
|
|
5630
6214
|
{
|
|
5631
6215
|
type: "text",
|
|
5632
|
-
text:
|
|
6216
|
+
text: `No locations registered. Use register_location to add sub-accounts.${fbSection2}`
|
|
5633
6217
|
}
|
|
5634
6218
|
]
|
|
5635
6219
|
};
|
|
5636
6220
|
}
|
|
5637
|
-
const
|
|
6221
|
+
const tokens = registry2.listLocations().map((loc) => ({
|
|
6222
|
+
...loc,
|
|
6223
|
+
companyId: registry2.getToken(loc.locationId)?.companyId
|
|
6224
|
+
}));
|
|
5638
6225
|
const currentId = client.defaultLocationId;
|
|
5639
|
-
const lines =
|
|
5640
|
-
|
|
5641
|
-
|
|
6226
|
+
const lines = tokens.map((loc) => {
|
|
6227
|
+
const marker = loc.locationId === currentId ? "\u2192 " : " ";
|
|
6228
|
+
const company = loc.companyId ? ` [company ${loc.companyId}${companiesWithFirebase.has(loc.companyId) ? ", Firebase \u2713" : ", no Firebase"}]` : "";
|
|
6229
|
+
return `${marker}${loc.name} (${loc.locationId})${company}`;
|
|
6230
|
+
});
|
|
6231
|
+
const fbSection = companies.length ? `
|
|
6232
|
+
|
|
6233
|
+
Company Firebase credentials (${companies.length}) \u2014 workflow builder works in these companies' sub-accounts:
|
|
6234
|
+
${companies.map((c) => ` ${c.name || c.companyId} (${c.companyId})`).join("\n")}` : "\n\nNo company Firebase credentials registered. To use the workflow builder in a client's GHL, run register_company_firebase.";
|
|
5642
6235
|
return {
|
|
5643
6236
|
content: [
|
|
5644
6237
|
{
|
|
5645
6238
|
type: "text",
|
|
5646
|
-
text: `Token Registry (${
|
|
6239
|
+
text: `Token Registry (${tokens.length} locations):
|
|
5647
6240
|
${lines.join("\n")}
|
|
5648
6241
|
|
|
5649
|
-
\u2192 = currently active`
|
|
6242
|
+
\u2192 = currently active${fbSection}`
|
|
5650
6243
|
}
|
|
5651
6244
|
]
|
|
5652
6245
|
};
|
|
@@ -5656,8 +6249,8 @@ ${lines.join("\n")}
|
|
|
5656
6249
|
"list_available_locations",
|
|
5657
6250
|
"List all GHL sub-accounts (locations) accessible with the current or agency API key. Shows locations that exist in the GHL account \u2014 use register_location to add their tokens. Offset-based pagination via skip/limit.",
|
|
5658
6251
|
{
|
|
5659
|
-
limit:
|
|
5660
|
-
skip:
|
|
6252
|
+
limit: import_zod38.z.number().optional().describe("Max locations to return. Defaults to 20."),
|
|
6253
|
+
skip: import_zod38.z.number().optional().describe("Number to skip for pagination.")
|
|
5661
6254
|
},
|
|
5662
6255
|
async ({ limit, skip }) => {
|
|
5663
6256
|
try {
|
|
@@ -5700,7 +6293,7 @@ ${lines.join("\n")}
|
|
|
5700
6293
|
}
|
|
5701
6294
|
|
|
5702
6295
|
// src/tools/bulk-operations.ts
|
|
5703
|
-
var
|
|
6296
|
+
var import_zod39 = require("zod");
|
|
5704
6297
|
function delay(ms) {
|
|
5705
6298
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
5706
6299
|
}
|
|
@@ -5712,8 +6305,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
5712
6305
|
"bulk_add_tags",
|
|
5713
6306
|
"Add tags to multiple contacts at once. Rate-limited to avoid API throttling. Returns a summary of successes and failures.",
|
|
5714
6307
|
{
|
|
5715
|
-
contactIds:
|
|
5716
|
-
tags:
|
|
6308
|
+
contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to tag."),
|
|
6309
|
+
tags: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one tag required.").describe("Tags to add to each contact.")
|
|
5717
6310
|
},
|
|
5718
6311
|
async ({ contactIds, tags }) => {
|
|
5719
6312
|
const results = { success: 0, failed: 0, errors: [] };
|
|
@@ -5735,8 +6328,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
5735
6328
|
"bulk_remove_tags",
|
|
5736
6329
|
"Remove tags from multiple contacts at once. Rate-limited.",
|
|
5737
6330
|
{
|
|
5738
|
-
contactIds:
|
|
5739
|
-
tags:
|
|
6331
|
+
contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs."),
|
|
6332
|
+
tags: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one tag required.").describe("Tags to remove from each contact.")
|
|
5740
6333
|
},
|
|
5741
6334
|
async ({ contactIds, tags }) => {
|
|
5742
6335
|
const results = { success: 0, failed: 0, errors: [] };
|
|
@@ -5757,8 +6350,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
5757
6350
|
"bulk_update_contacts",
|
|
5758
6351
|
"Update the same field(s) on multiple contacts at once. Rate-limited. Example: set a custom field value, change source, update address for a batch of contacts.",
|
|
5759
6352
|
{
|
|
5760
|
-
contactIds:
|
|
5761
|
-
fields:
|
|
6353
|
+
contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
|
|
6354
|
+
fields: import_zod39.z.record(import_zod39.z.unknown()).describe("Fields to set on each contact (e.g. {customField: {id: 'xxx', value: 'yyy'}}, {source: 'Import'}).")
|
|
5762
6355
|
},
|
|
5763
6356
|
async ({ contactIds, fields }) => {
|
|
5764
6357
|
const results = { success: 0, failed: 0, errors: [] };
|
|
@@ -5779,8 +6372,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
5779
6372
|
"bulk_add_to_workflow",
|
|
5780
6373
|
"Enroll multiple contacts into a workflow at once. Rate-limited.",
|
|
5781
6374
|
{
|
|
5782
|
-
contactIds:
|
|
5783
|
-
workflowId:
|
|
6375
|
+
contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to enroll."),
|
|
6376
|
+
workflowId: import_zod39.z.string().describe("The workflow ID to enroll contacts into.")
|
|
5784
6377
|
},
|
|
5785
6378
|
async ({ contactIds, workflowId }) => {
|
|
5786
6379
|
const results = { success: 0, failed: 0, errors: [] };
|
|
@@ -5801,8 +6394,8 @@ function registerBulkOperationTools(server2, client) {
|
|
|
5801
6394
|
"bulk_delete_contacts",
|
|
5802
6395
|
"Delete multiple contacts at once. IRREVERSIBLE. Rate-limited. Use with extreme caution.",
|
|
5803
6396
|
{
|
|
5804
|
-
contactIds:
|
|
5805
|
-
confirm:
|
|
6397
|
+
contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to permanently delete."),
|
|
6398
|
+
confirm: import_zod39.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
|
|
5806
6399
|
},
|
|
5807
6400
|
async ({ contactIds, confirm }) => {
|
|
5808
6401
|
if (confirm !== "DELETE") {
|
|
@@ -5825,7 +6418,7 @@ function registerBulkOperationTools(server2, client) {
|
|
|
5825
6418
|
}
|
|
5826
6419
|
|
|
5827
6420
|
// src/tools/account-export.ts
|
|
5828
|
-
var
|
|
6421
|
+
var import_zod40 = require("zod");
|
|
5829
6422
|
function delay2(ms) {
|
|
5830
6423
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
5831
6424
|
}
|
|
@@ -5835,8 +6428,8 @@ function registerAccountExportTools(server2, client) {
|
|
|
5835
6428
|
"export_account",
|
|
5836
6429
|
"Export a complete inventory of the GHL sub-account: location info, contacts (count + sample), pipelines with stages, workflows (with full actions if builder auth is configured), funnels with pages, forms, custom fields, custom values, tags, calendars, and users. Returns a comprehensive JSON report for auditing or backup.",
|
|
5837
6430
|
{
|
|
5838
|
-
locationId:
|
|
5839
|
-
includeContacts:
|
|
6431
|
+
locationId: import_zod40.z.string().optional().describe("Location ID to export. Uses default if not specified."),
|
|
6432
|
+
includeContacts: import_zod40.z.boolean().optional().describe("Include contact list (first 100). Defaults to false for speed.")
|
|
5840
6433
|
},
|
|
5841
6434
|
async ({ locationId: locationId2, includeContacts }) => {
|
|
5842
6435
|
try {
|
|
@@ -5964,8 +6557,8 @@ function registerAccountExportTools(server2, client) {
|
|
|
5964
6557
|
"compare_locations",
|
|
5965
6558
|
"Compare two GHL sub-accounts side by side \u2014 shows differences in pipelines, workflows, custom fields, tags, forms, and funnels. Useful for ensuring consistency across locations or auditing before/after changes.",
|
|
5966
6559
|
{
|
|
5967
|
-
locationA:
|
|
5968
|
-
locationB:
|
|
6560
|
+
locationA: import_zod40.z.string().describe("First Location ID."),
|
|
6561
|
+
locationB: import_zod40.z.string().describe("Second Location ID.")
|
|
5969
6562
|
},
|
|
5970
6563
|
async ({ locationA, locationB }) => {
|
|
5971
6564
|
try {
|
|
@@ -6043,8 +6636,8 @@ function registerAccountExportTools(server2, client) {
|
|
|
6043
6636
|
}
|
|
6044
6637
|
|
|
6045
6638
|
// src/tools/workflow-cloner.ts
|
|
6046
|
-
var
|
|
6047
|
-
var
|
|
6639
|
+
var import_zod41 = require("zod");
|
|
6640
|
+
var crypto3 = __toESM(require("crypto"));
|
|
6048
6641
|
function registerWorkflowClonerTools(server2, builderClient) {
|
|
6049
6642
|
const client = builderClient;
|
|
6050
6643
|
if (!client) return;
|
|
@@ -6052,8 +6645,8 @@ function registerWorkflowClonerTools(server2, builderClient) {
|
|
|
6052
6645
|
"clone_workflow",
|
|
6053
6646
|
"Deep clone a workflow \u2014 creates an exact copy with new IDs for all actions, triggers, and references. The clone starts as a draft. Useful for creating templates or duplicating workflows across projects.",
|
|
6054
6647
|
{
|
|
6055
|
-
sourceWorkflowId:
|
|
6056
|
-
newName:
|
|
6648
|
+
sourceWorkflowId: import_zod41.z.string().describe("The workflow ID to clone."),
|
|
6649
|
+
newName: import_zod41.z.string().describe("Name for the cloned workflow.")
|
|
6057
6650
|
},
|
|
6058
6651
|
async ({ sourceWorkflowId, newName }) => {
|
|
6059
6652
|
try {
|
|
@@ -6063,12 +6656,12 @@ function registerWorkflowClonerTools(server2, builderClient) {
|
|
|
6063
6656
|
const triggers = source.triggers || [];
|
|
6064
6657
|
for (const action of actions) {
|
|
6065
6658
|
if (action.id) {
|
|
6066
|
-
idMap.set(action.id,
|
|
6659
|
+
idMap.set(action.id, crypto3.randomUUID());
|
|
6067
6660
|
}
|
|
6068
6661
|
}
|
|
6069
6662
|
for (const trigger of triggers) {
|
|
6070
6663
|
if (trigger.id) {
|
|
6071
|
-
idMap.set(trigger.id,
|
|
6664
|
+
idMap.set(trigger.id, crypto3.randomUUID());
|
|
6072
6665
|
}
|
|
6073
6666
|
}
|
|
6074
6667
|
const remap = (id) => {
|
|
@@ -6142,7 +6735,7 @@ function registerWorkflowClonerTools(server2, builderClient) {
|
|
|
6142
6735
|
}
|
|
6143
6736
|
|
|
6144
6737
|
// src/tools/smart-lists.ts
|
|
6145
|
-
var
|
|
6738
|
+
var import_zod42 = require("zod");
|
|
6146
6739
|
var SMARTLIST_BASE = "https://backend.leadconnectorhq.com/lists/dynamic";
|
|
6147
6740
|
var OBJECT_KEYS = ["contacts", "opportunity"];
|
|
6148
6741
|
function registerSmartListTools(server2, builderClient) {
|
|
@@ -6169,11 +6762,11 @@ ${text2}`);
|
|
|
6169
6762
|
"list_smart_lists",
|
|
6170
6763
|
"List smart lists (dynamic / saved-filter lists) in a location. Smart Lists are saved searches over contacts or opportunities \u2014 agencies use them to segment by complex criteria. Filters and columns aren't returned in the list view; use get_smart_list for the full filter spec.",
|
|
6171
6764
|
{
|
|
6172
|
-
objectKey:
|
|
6173
|
-
query:
|
|
6174
|
-
limit:
|
|
6175
|
-
startAfter:
|
|
6176
|
-
locationId:
|
|
6765
|
+
objectKey: import_zod42.z.enum(OBJECT_KEYS).describe("The object type the lists segment over. 'contacts' for contact-segments, 'opportunity' for opportunity-segments. Required \u2014 GHL rejects requests without it."),
|
|
6766
|
+
query: import_zod42.z.string().optional().describe("Free-text search across smart list names."),
|
|
6767
|
+
limit: import_zod42.z.number().optional().describe("Max smart lists per page. Defaults to 20 on GHL's side."),
|
|
6768
|
+
startAfter: import_zod42.z.string().optional().describe("Cursor for pagination \u2014 pass the last list's id from the previous page."),
|
|
6769
|
+
locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6177
6770
|
},
|
|
6178
6771
|
async ({ objectKey, query, limit, startAfter, locationId: locationId2 }) => {
|
|
6179
6772
|
try {
|
|
@@ -6193,8 +6786,8 @@ ${text2}`);
|
|
|
6193
6786
|
"get_smart_list",
|
|
6194
6787
|
"Get a single smart list by ID with its full configuration: filters, columns, permissions, and metadata. The filters array is what defines who/what is in the list.",
|
|
6195
6788
|
{
|
|
6196
|
-
listId:
|
|
6197
|
-
locationId:
|
|
6789
|
+
listId: import_zod42.z.string().describe("The smart list ID (from list_smart_lists or a previous create_smart_list response)."),
|
|
6790
|
+
locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6198
6791
|
},
|
|
6199
6792
|
async ({ listId, locationId: locationId2 }) => {
|
|
6200
6793
|
try {
|
|
@@ -6210,13 +6803,13 @@ ${text2}`);
|
|
|
6210
6803
|
"create_smart_list",
|
|
6211
6804
|
"Create a new smart list (dynamic filter list). Required: name + objectKey. Filters define the saved-search criteria \u2014 pass an empty array to create an empty list and add filters later via update_smart_list. The shape of filters/columns is opaque here; query an existing smart list with get_smart_list to see the format GHL expects.",
|
|
6212
6805
|
{
|
|
6213
|
-
name:
|
|
6214
|
-
objectKey:
|
|
6215
|
-
filters:
|
|
6216
|
-
columns:
|
|
6217
|
-
pipelineIds:
|
|
6218
|
-
defaultInPipelines:
|
|
6219
|
-
locationId:
|
|
6806
|
+
name: import_zod42.z.string().describe("Display name for the smart list."),
|
|
6807
|
+
objectKey: import_zod42.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
|
|
6808
|
+
filters: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Array of filter objects. Each object has fields like {field, operator, value} \u2014 exact shape varies by filter type. See get_smart_list on an existing list to learn the format."),
|
|
6809
|
+
columns: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Array of column definitions for the smart list view in GHL UI. Each defines which contact/opportunity field shows as a column. Defaults to GHL's standard columns if omitted."),
|
|
6810
|
+
pipelineIds: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs to restrict this smart list to. Empty array = all pipelines."),
|
|
6811
|
+
defaultInPipelines: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
|
|
6812
|
+
locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6220
6813
|
},
|
|
6221
6814
|
async ({ name, objectKey, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
|
|
6222
6815
|
try {
|
|
@@ -6237,13 +6830,13 @@ ${text2}`);
|
|
|
6237
6830
|
"update_smart_list",
|
|
6238
6831
|
"Update an existing smart list's name, filters, or columns. The objectKey CANNOT be changed after creation (GHL rejects with 422 if you try). Use get_smart_list first to inspect the current filters; partial updates work \u2014 pass only the fields you want to change.",
|
|
6239
6832
|
{
|
|
6240
|
-
listId:
|
|
6241
|
-
name:
|
|
6242
|
-
filters:
|
|
6243
|
-
columns:
|
|
6244
|
-
pipelineIds:
|
|
6245
|
-
defaultInPipelines:
|
|
6246
|
-
locationId:
|
|
6833
|
+
listId: import_zod42.z.string().describe("The smart list ID to update."),
|
|
6834
|
+
name: import_zod42.z.string().optional().describe("New display name."),
|
|
6835
|
+
filters: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Replace the filter array entirely. To add a filter, fetch the current list, append, and pass the new array."),
|
|
6836
|
+
columns: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Replace the column array entirely."),
|
|
6837
|
+
pipelineIds: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
|
|
6838
|
+
defaultInPipelines: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
|
|
6839
|
+
locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6247
6840
|
},
|
|
6248
6841
|
async ({ listId, name, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
|
|
6249
6842
|
try {
|
|
@@ -6268,9 +6861,9 @@ ${text2}`);
|
|
|
6268
6861
|
"delete_smart_list",
|
|
6269
6862
|
"Permanently delete a smart list. IRREVERSIBLE. The list configuration is removed but the contacts/opportunities themselves are NOT touched \u2014 smart lists are just saved filters. Any workflow trigger / dashboard / report that referenced this list by ID will stop working.",
|
|
6270
6863
|
{
|
|
6271
|
-
listId:
|
|
6272
|
-
confirm:
|
|
6273
|
-
locationId:
|
|
6864
|
+
listId: import_zod42.z.string().describe("The smart list ID to delete."),
|
|
6865
|
+
confirm: import_zod42.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
|
|
6866
|
+
locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6274
6867
|
},
|
|
6275
6868
|
async ({ listId, locationId: locationId2 }) => {
|
|
6276
6869
|
try {
|
|
@@ -6285,7 +6878,7 @@ ${text2}`);
|
|
|
6285
6878
|
}
|
|
6286
6879
|
|
|
6287
6880
|
// src/tools/reputation.ts
|
|
6288
|
-
var
|
|
6881
|
+
var import_zod43 = require("zod");
|
|
6289
6882
|
var REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
|
|
6290
6883
|
function registerReputationTools(server2, builderClient) {
|
|
6291
6884
|
const client = builderClient;
|
|
@@ -6306,7 +6899,7 @@ ${text2}`);
|
|
|
6306
6899
|
"get_review_link_list",
|
|
6307
6900
|
"List the review-link destinations configured for a location \u2014 the platforms (Google, Facebook, etc.) where review requests send contacts. Each entry has a label and the public review URL. Useful for: building review-request workflows (the workflow goal condition `review_request_clicked` references these review-link ids), and auditing which review platforms a sub-account has connected. NOTE: listing the actual reviews that come BACK (and responding to them) is not yet available \u2014 that endpoint needs a DevTools capture against a live account with connected review platforms.",
|
|
6308
6901
|
{
|
|
6309
|
-
locationId:
|
|
6902
|
+
locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6310
6903
|
},
|
|
6311
6904
|
async ({ locationId: locationId2 }) => {
|
|
6312
6905
|
try {
|
|
@@ -6321,7 +6914,7 @@ ${text2}`);
|
|
|
6321
6914
|
}
|
|
6322
6915
|
|
|
6323
6916
|
// src/tools/email-campaigns.ts
|
|
6324
|
-
var
|
|
6917
|
+
var import_zod44 = require("zod");
|
|
6325
6918
|
var SVC_BASE = "https://services.leadconnectorhq.com";
|
|
6326
6919
|
function registerEmailCampaignTools(server2, builderClient) {
|
|
6327
6920
|
const client = builderClient;
|
|
@@ -6330,15 +6923,15 @@ function registerEmailCampaignTools(server2, builderClient) {
|
|
|
6330
6923
|
"create_email_campaign",
|
|
6331
6924
|
"Create an email campaign / broadcast DRAFT from an existing email template. Requires a templateId (create one first with create_email_template). The campaign is created as a draft \u2014 to actually SEND or schedule it, finish in the GHL UI: the send/schedule endpoint isn't available through the API yet. There's also no API delete for campaigns, so drafts created here are removed via the GHL UI. Despite those limits, this gets the campaign 90% built \u2014 template, subject, sender, name all set programmatically.",
|
|
6332
6925
|
{
|
|
6333
|
-
templateId:
|
|
6334
|
-
name:
|
|
6335
|
-
subject:
|
|
6336
|
-
fromName:
|
|
6337
|
-
fromEmail:
|
|
6338
|
-
isPlainText:
|
|
6339
|
-
enableResendToUnopened:
|
|
6340
|
-
hasUtmTracking:
|
|
6341
|
-
locationId:
|
|
6926
|
+
templateId: import_zod44.z.string().describe("ID of an email template (from create_email_template or list_email_templates) to use as the campaign body."),
|
|
6927
|
+
name: import_zod44.z.string().optional().describe("Internal campaign name (shown in the campaigns list, not to recipients). Defaults to a GHL-generated name."),
|
|
6928
|
+
subject: import_zod44.z.string().optional().describe("Email subject line recipients see."),
|
|
6929
|
+
fromName: import_zod44.z.string().optional().describe("Sender display name."),
|
|
6930
|
+
fromEmail: import_zod44.z.string().optional().describe("Sender email address. Must be a verified sending address in the location."),
|
|
6931
|
+
isPlainText: import_zod44.z.boolean().optional().describe("Send as plain text instead of HTML. Defaults to false."),
|
|
6932
|
+
enableResendToUnopened: import_zod44.z.boolean().optional().describe("Auto-resend to contacts who didn't open. Defaults to false."),
|
|
6933
|
+
hasUtmTracking: import_zod44.z.boolean().optional().describe("Append UTM tracking params to links. Defaults to false."),
|
|
6934
|
+
locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6342
6935
|
},
|
|
6343
6936
|
async ({ templateId, name, subject, fromName, fromEmail, isPlainText, enableResendToUnopened, hasUtmTracking, locationId: locationId2 }) => {
|
|
6344
6937
|
try {
|
|
@@ -6376,7 +6969,7 @@ ${text2}`);
|
|
|
6376
6969
|
}
|
|
6377
6970
|
|
|
6378
6971
|
// src/tools/memberships.ts
|
|
6379
|
-
var
|
|
6972
|
+
var import_zod45 = require("zod");
|
|
6380
6973
|
var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
|
|
6381
6974
|
function registerMembershipTools(server2, builderClient) {
|
|
6382
6975
|
const client = builderClient;
|
|
@@ -6397,7 +6990,7 @@ ${text2}`);
|
|
|
6397
6990
|
"list_membership_offers",
|
|
6398
6991
|
"List a location's membership offers and products in one call. Returns { products: [...], offers: [...] }. Products are courses/communities; offers are the access grants (what a contact gets enrolled in). Use the returned ids with membership trigger conditions like offer_access_granted / product_completed. READ-ONLY \u2014 creating offers/products happens in the GHL Memberships UI.",
|
|
6399
6992
|
{
|
|
6400
|
-
locationId:
|
|
6993
|
+
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6401
6994
|
},
|
|
6402
6995
|
async ({ locationId: locationId2 }) => {
|
|
6403
6996
|
try {
|
|
@@ -6413,8 +7006,8 @@ ${text2}`);
|
|
|
6413
7006
|
"list_membership_categories",
|
|
6414
7007
|
"List all membership/course categories in a location. Categories group lessons inside a course/product. Use the returned ids with the category_completed / category_started trigger conditions. READ-ONLY.",
|
|
6415
7008
|
{
|
|
6416
|
-
limit:
|
|
6417
|
-
locationId:
|
|
7009
|
+
limit: import_zod45.z.number().optional().describe("Max categories to return. Defaults to a large value (effectively all)."),
|
|
7010
|
+
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6418
7011
|
},
|
|
6419
7012
|
async ({ limit, locationId: locationId2 }) => {
|
|
6420
7013
|
try {
|
|
@@ -6430,8 +7023,8 @@ ${text2}`);
|
|
|
6430
7023
|
"list_membership_lessons",
|
|
6431
7024
|
"List all membership/course lessons in a location. Use the returned ids with the lesson_completed / lesson_started trigger conditions. READ-ONLY.",
|
|
6432
7025
|
{
|
|
6433
|
-
limit:
|
|
6434
|
-
locationId:
|
|
7026
|
+
limit: import_zod45.z.number().optional().describe("Max lessons to return. Defaults to a large value (effectively all)."),
|
|
7027
|
+
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6435
7028
|
},
|
|
6436
7029
|
async ({ limit, locationId: locationId2 }) => {
|
|
6437
7030
|
try {
|
|
@@ -6446,41 +7039,41 @@ ${text2}`);
|
|
|
6446
7039
|
}
|
|
6447
7040
|
|
|
6448
7041
|
// src/tools/template-deployer.ts
|
|
6449
|
-
var
|
|
7042
|
+
var import_zod46 = require("zod");
|
|
6450
7043
|
var fs4 = __toESM(require("fs"));
|
|
6451
7044
|
var path4 = __toESM(require("path"));
|
|
6452
7045
|
function delay3(ms) {
|
|
6453
7046
|
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
6454
7047
|
}
|
|
6455
|
-
var TemplateSchema =
|
|
6456
|
-
templateName:
|
|
6457
|
-
templateVersion:
|
|
6458
|
-
description:
|
|
6459
|
-
questionnaire:
|
|
6460
|
-
id:
|
|
6461
|
-
question:
|
|
6462
|
-
type:
|
|
6463
|
-
required:
|
|
6464
|
-
placeholder:
|
|
7048
|
+
var TemplateSchema = import_zod46.z.object({
|
|
7049
|
+
templateName: import_zod46.z.string(),
|
|
7050
|
+
templateVersion: import_zod46.z.string().optional(),
|
|
7051
|
+
description: import_zod46.z.string().optional().default(""),
|
|
7052
|
+
questionnaire: import_zod46.z.array(import_zod46.z.object({
|
|
7053
|
+
id: import_zod46.z.string(),
|
|
7054
|
+
question: import_zod46.z.string(),
|
|
7055
|
+
type: import_zod46.z.string(),
|
|
7056
|
+
required: import_zod46.z.boolean().optional(),
|
|
7057
|
+
placeholder: import_zod46.z.string().optional()
|
|
6465
7058
|
})).optional().default([]),
|
|
6466
|
-
location:
|
|
6467
|
-
tags:
|
|
6468
|
-
customFields:
|
|
6469
|
-
name:
|
|
6470
|
-
dataType:
|
|
7059
|
+
location: import_zod46.z.record(import_zod46.z.unknown()).optional(),
|
|
7060
|
+
tags: import_zod46.z.array(import_zod46.z.string()).optional(),
|
|
7061
|
+
customFields: import_zod46.z.array(import_zod46.z.object({
|
|
7062
|
+
name: import_zod46.z.string(),
|
|
7063
|
+
dataType: import_zod46.z.string()
|
|
6471
7064
|
})).optional(),
|
|
6472
|
-
pipelines:
|
|
6473
|
-
name:
|
|
6474
|
-
stages:
|
|
7065
|
+
pipelines: import_zod46.z.array(import_zod46.z.object({
|
|
7066
|
+
name: import_zod46.z.string(),
|
|
7067
|
+
stages: import_zod46.z.array(import_zod46.z.object({ position: import_zod46.z.number(), name: import_zod46.z.string() }))
|
|
6475
7068
|
})).optional(),
|
|
6476
|
-
workflows:
|
|
6477
|
-
name:
|
|
6478
|
-
condition:
|
|
6479
|
-
actions:
|
|
7069
|
+
workflows: import_zod46.z.array(import_zod46.z.object({
|
|
7070
|
+
name: import_zod46.z.string(),
|
|
7071
|
+
condition: import_zod46.z.string().optional(),
|
|
7072
|
+
actions: import_zod46.z.array(import_zod46.z.record(import_zod46.z.unknown())).optional().default([])
|
|
6480
7073
|
})).optional(),
|
|
6481
|
-
calendars:
|
|
6482
|
-
name:
|
|
6483
|
-
description:
|
|
7074
|
+
calendars: import_zod46.z.array(import_zod46.z.object({
|
|
7075
|
+
name: import_zod46.z.string(),
|
|
7076
|
+
description: import_zod46.z.string().optional()
|
|
6484
7077
|
})).optional()
|
|
6485
7078
|
});
|
|
6486
7079
|
function registerTemplateDeployerTools(server2, client) {
|
|
@@ -6551,7 +7144,7 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
6551
7144
|
"get_template_questionnaire",
|
|
6552
7145
|
"Get the questionnaire for a specific template. Returns all the questions that need to be answered before deploying. Present these to the user one at a time in a conversational style.",
|
|
6553
7146
|
{
|
|
6554
|
-
templateFile:
|
|
7147
|
+
templateFile: import_zod46.z.string().describe("Path to the template JSON file (from list_templates).")
|
|
6555
7148
|
},
|
|
6556
7149
|
async ({ templateFile }) => {
|
|
6557
7150
|
try {
|
|
@@ -6584,10 +7177,10 @@ function registerTemplateDeployerTools(server2, client) {
|
|
|
6584
7177
|
"deploy_template",
|
|
6585
7178
|
"Deploy a template to set up a GHL sub-account. Creates tags, custom fields, pipelines with stages, calendars, workflows, and forms based on the template and the user's questionnaire answers. This is the main setup automation tool.",
|
|
6586
7179
|
{
|
|
6587
|
-
templateFile:
|
|
6588
|
-
answers:
|
|
6589
|
-
locationId:
|
|
6590
|
-
dryRun:
|
|
7180
|
+
templateFile: import_zod46.z.string().describe("Path to the template JSON file."),
|
|
7181
|
+
answers: import_zod46.z.record(import_zod46.z.unknown()).describe("Questionnaire answers keyed by question ID (e.g. {business_name: 'My Clinic', business_phone: '+15551234567', ...})."),
|
|
7182
|
+
locationId: import_zod46.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
|
|
7183
|
+
dryRun: import_zod46.z.boolean().optional().describe("If true, shows what would be created without actually creating anything. Defaults to false.")
|
|
6591
7184
|
},
|
|
6592
7185
|
async ({ templateFile, answers, locationId: locationId2, dryRun }) => {
|
|
6593
7186
|
try {
|
|
@@ -6833,7 +7426,7 @@ ${errors.join("\n")}` : "\nNo errors!",
|
|
|
6833
7426
|
}
|
|
6834
7427
|
|
|
6835
7428
|
// src/tools/validators.ts
|
|
6836
|
-
var
|
|
7429
|
+
var import_zod47 = require("zod");
|
|
6837
7430
|
function extractFromTrigger(trigger, refs) {
|
|
6838
7431
|
const triggerName = String(trigger.name ?? trigger.type ?? "unnamed trigger");
|
|
6839
7432
|
const where = `trigger "${triggerName}"`;
|
|
@@ -6968,7 +7561,7 @@ function registerValidatorTools(server2, client, builderClient) {
|
|
|
6968
7561
|
"validate_workflow",
|
|
6969
7562
|
"Pre-flight ID validation for a 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 this BEFORE publish_workflow when a workflow has been edited, or whenever a published workflow stops behaving as expected. Catches the silent-failure bug where invalid IDs make GHL skip all subsequent actions without warning. Returns a structured report of valid + missing references.",
|
|
6970
7563
|
{
|
|
6971
|
-
workflowId:
|
|
7564
|
+
workflowId: import_zod47.z.string().describe("The workflow ID to validate.")
|
|
6972
7565
|
},
|
|
6973
7566
|
async ({ workflowId }) => {
|
|
6974
7567
|
try {
|
|
@@ -7239,20 +7832,24 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
|
|
|
7239
7832
|
return { name: "Firebase auth (workflow builder)", status: "skip", detail: "Not configured. The 8 workflow builder tools need Firebase credentials. The other 168 tools work fine without. To add it: run enable_workflow_builder with the three Firebase values from your GHL browser session (see elitedcs.com/ghl-mcp-firebase for DevTools steps)." };
|
|
7240
7833
|
}
|
|
7241
7834
|
const result = await builderClient.checkAuth();
|
|
7835
|
+
const activeCompany = builderClient.getCurrentCompanyId();
|
|
7836
|
+
const companyNote = activeCompany ? ` Active company: ${activeCompany}.` : "";
|
|
7242
7837
|
if (result.ok) {
|
|
7243
|
-
return { name: "Firebase auth (workflow builder)", status: "pass", detail:
|
|
7838
|
+
return { name: "Firebase auth (workflow builder)", status: "pass", detail: `ID token refresh succeeded. Workflow builder tools are usable.${companyNote}` };
|
|
7244
7839
|
}
|
|
7245
|
-
return { name: "Firebase auth (workflow builder)", status: "fail", detail: `Token refresh failed: ${result.error}.
|
|
7840
|
+
return { name: "Firebase auth (workflow builder)", status: "fail", detail: `Token refresh failed: ${result.error}.${companyNote} If you're in a client's account, register its Firebase with register_company_firebase. Otherwise re-capture your Firebase values from GHL DevTools and re-run enable_workflow_builder.` };
|
|
7246
7841
|
})();
|
|
7247
7842
|
const registryCheck = (() => {
|
|
7248
7843
|
if (!registry2) {
|
|
7249
7844
|
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." };
|
|
7250
7845
|
}
|
|
7251
7846
|
const locs = registry2.listLocations();
|
|
7847
|
+
const companies = registry2.listCompanyFirebases();
|
|
7848
|
+
const companyNote = companies.length ? ` ${companies.length} company Firebase credential(s) registered for multi-tenant workflow-builder access.` : "";
|
|
7252
7849
|
if (locs.length === 0) {
|
|
7253
|
-
return { name: "Token registry", status: "warn", detail:
|
|
7850
|
+
return { name: "Token registry", status: "warn", detail: `Initialized but empty \u2014 register sub-accounts via register_location for cross-account switching.${companyNote}` };
|
|
7254
7851
|
}
|
|
7255
|
-
return { name: "Token registry", status: "pass", detail: `${locs.length} sub-account(s) registered
|
|
7852
|
+
return { name: "Token registry", status: "pass", detail: `${locs.length} sub-account(s) registered.${companyNote}` };
|
|
7256
7853
|
})();
|
|
7257
7854
|
const [versionStatus, apiKeyCheck, locationCheck, firebaseCheck] = await Promise.all([
|
|
7258
7855
|
versionPromise,
|
|
@@ -7339,226 +7936,13 @@ function registerAllTools(server2, client, registry2, mcpVersion) {
|
|
|
7339
7936
|
registerSmartListTools(server2, builderClient);
|
|
7340
7937
|
registerReputationTools(server2, builderClient);
|
|
7341
7938
|
registerEmailCampaignTools(server2, builderClient);
|
|
7939
|
+
registerEmailBuilderInternalTools(server2, builderClient);
|
|
7342
7940
|
registerMembershipTools(server2, builderClient);
|
|
7343
7941
|
registerValidatorTools(server2, client, builderClient);
|
|
7344
7942
|
registerDiagnosticTools(server2, mcpVersion ?? "unknown", client, builderClient, registry2 ?? null);
|
|
7345
7943
|
registerLocationSwitcherTools(server2, client, builderClient, registry2, mcpVersion);
|
|
7346
7944
|
}
|
|
7347
7945
|
|
|
7348
|
-
// src/setup-tool.ts
|
|
7349
|
-
var os2 = __toESM(require("os"));
|
|
7350
|
-
var crypto3 = __toESM(require("crypto"));
|
|
7351
|
-
var import_zod47 = require("zod");
|
|
7352
|
-
var LICENSE_API = "https://elitedcs.com/api/validate-license";
|
|
7353
|
-
var GHL_API = "https://services.leadconnectorhq.com";
|
|
7354
|
-
var FIREBASE_TOKEN_API = "https://securetoken.googleapis.com/v1/token";
|
|
7355
|
-
function deviceFingerprint() {
|
|
7356
|
-
const raw = `${os2.hostname()}:${os2.userInfo().username}:${os2.platform()}:${os2.arch()}`;
|
|
7357
|
-
return crypto3.createHash("sha256").update(raw).digest("hex").slice(0, 16);
|
|
7358
|
-
}
|
|
7359
|
-
async function validateLicense(email, licenseKey) {
|
|
7360
|
-
try {
|
|
7361
|
-
const res = await fetch(LICENSE_API, {
|
|
7362
|
-
method: "POST",
|
|
7363
|
-
headers: { "Content-Type": "application/json" },
|
|
7364
|
-
body: JSON.stringify({
|
|
7365
|
-
email: email.trim(),
|
|
7366
|
-
license_key: licenseKey.trim(),
|
|
7367
|
-
device_fingerprint: deviceFingerprint()
|
|
7368
|
-
}),
|
|
7369
|
-
signal: AbortSignal.timeout(1e4)
|
|
7370
|
-
});
|
|
7371
|
-
const data = await res.json().catch(() => ({}));
|
|
7372
|
-
if (res.ok && data.valid) {
|
|
7373
|
-
return { ok: true, installs: `${data.installs_used}/${data.installs_max}` };
|
|
7374
|
-
}
|
|
7375
|
-
return { ok: false, error: data.error || data.message || `License validation failed (HTTP ${res.status})` };
|
|
7376
|
-
} catch (err) {
|
|
7377
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
7378
|
-
return { ok: false, error: `Could not reach license server: ${msg}` };
|
|
7379
|
-
}
|
|
7380
|
-
}
|
|
7381
|
-
async function validateGhl(apiKey2, locationId2) {
|
|
7382
|
-
try {
|
|
7383
|
-
const res = await fetch(`${GHL_API}/locations/${locationId2}`, {
|
|
7384
|
-
headers: {
|
|
7385
|
-
Authorization: `Bearer ${apiKey2}`,
|
|
7386
|
-
Version: "2021-07-28",
|
|
7387
|
-
Accept: "application/json"
|
|
7388
|
-
},
|
|
7389
|
-
signal: AbortSignal.timeout(1e4)
|
|
7390
|
-
});
|
|
7391
|
-
if (res.status === 401) return { ok: false, error: "GHL API key is invalid (401)." };
|
|
7392
|
-
if (res.status === 403) return { ok: false, error: "GHL API key doesn't have access to this Location ID. The key must be created INSIDE this sub-account." };
|
|
7393
|
-
if (!res.ok) return { ok: false, error: `GHL returned HTTP ${res.status}.` };
|
|
7394
|
-
const data = await res.json().catch(() => ({}));
|
|
7395
|
-
const name = data?.location?.name || data?.name || "Unknown";
|
|
7396
|
-
return { ok: true, locationName: name };
|
|
7397
|
-
} catch (err) {
|
|
7398
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
7399
|
-
return { ok: false, error: `Could not reach GHL: ${msg}` };
|
|
7400
|
-
}
|
|
7401
|
-
}
|
|
7402
|
-
async function validateFirebase(firebaseKey, refreshToken) {
|
|
7403
|
-
try {
|
|
7404
|
-
const res = await fetch(`${FIREBASE_TOKEN_API}?key=${encodeURIComponent(firebaseKey)}`, {
|
|
7405
|
-
method: "POST",
|
|
7406
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
7407
|
-
body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,
|
|
7408
|
-
signal: AbortSignal.timeout(1e4)
|
|
7409
|
-
});
|
|
7410
|
-
if (!res.ok) return { ok: false, error: "Firebase credentials rejected. Re-extract them from your GHL browser tab." };
|
|
7411
|
-
return { ok: true };
|
|
7412
|
-
} catch (err) {
|
|
7413
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
7414
|
-
return { ok: false, error: `Could not reach Firebase: ${msg}` };
|
|
7415
|
-
}
|
|
7416
|
-
}
|
|
7417
|
-
function registerSetupTool(server2) {
|
|
7418
|
-
server2.tool(
|
|
7419
|
-
"setup_ghl_mcp",
|
|
7420
|
-
"First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 201 tools (161 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
|
|
7421
|
-
{
|
|
7422
|
-
email: import_zod47.z.string().email().describe("Email used at purchase."),
|
|
7423
|
-
license_key: import_zod47.z.string().min(20).describe("License key from your purchase email."),
|
|
7424
|
-
ghl_api_key: import_zod47.z.string().min(10).describe("GHL Private Integration key (starts with 'pit-'). Created INSIDE the sub-account at Settings > Integrations > Private Integrations."),
|
|
7425
|
-
ghl_location_id: import_zod47.z.string().min(10).describe("GHL Location ID (sub-account ID). Found in your GHL URL: /location/THIS_PART/dashboard."),
|
|
7426
|
-
ghl_company_id: import_zod47.z.string().optional().describe("(Agency only) Company ID for multi-location access."),
|
|
7427
|
-
ghl_user_id: import_zod47.z.string().optional().describe("(Workflow Builder, optional) Firebase User ID. See README for browser capture instructions."),
|
|
7428
|
-
ghl_firebase_api_key: import_zod47.z.string().optional().describe("(Workflow Builder, optional) Firebase API Key starting with 'AIza'."),
|
|
7429
|
-
ghl_firebase_refresh_token: import_zod47.z.string().optional().describe("(Workflow Builder, optional) Firebase refresh token starting with 'AMf-'.")
|
|
7430
|
-
},
|
|
7431
|
-
async (args) => {
|
|
7432
|
-
const lic = await validateLicense(args.email, args.license_key);
|
|
7433
|
-
if (!lic.ok) {
|
|
7434
|
-
return { content: [{ type: "text", text: `License check failed: ${lic.error}
|
|
7435
|
-
|
|
7436
|
-
Purchase a license at https://elitedcs.com/ghl-mcp-server or contact support.` }], isError: true };
|
|
7437
|
-
}
|
|
7438
|
-
const ghl = await validateGhl(args.ghl_api_key, args.ghl_location_id);
|
|
7439
|
-
if (!ghl.ok) {
|
|
7440
|
-
return { content: [{ type: "text", text: `GHL credential check failed: ${ghl.error}` }], isError: true };
|
|
7441
|
-
}
|
|
7442
|
-
const wantsWorkflowBuilder = args.ghl_user_id || args.ghl_firebase_api_key || args.ghl_firebase_refresh_token;
|
|
7443
|
-
let workflowBuilderEnabled = false;
|
|
7444
|
-
let workflowBuilderNote = "";
|
|
7445
|
-
if (wantsWorkflowBuilder) {
|
|
7446
|
-
if (!args.ghl_user_id || !args.ghl_firebase_api_key || !args.ghl_firebase_refresh_token) {
|
|
7447
|
-
return {
|
|
7448
|
-
content: [{
|
|
7449
|
-
type: "text",
|
|
7450
|
-
text: "Workflow Builder requires ALL THREE Firebase fields: ghl_user_id, ghl_firebase_api_key, ghl_firebase_refresh_token. Either provide all three or omit all three. See https://elitedcs.com/ghl-mcp-server/firebase for one-click capture."
|
|
7451
|
-
}],
|
|
7452
|
-
isError: true
|
|
7453
|
-
};
|
|
7454
|
-
}
|
|
7455
|
-
const fb = await validateFirebase(args.ghl_firebase_api_key, args.ghl_firebase_refresh_token);
|
|
7456
|
-
if (!fb.ok) {
|
|
7457
|
-
workflowBuilderNote = `
|
|
7458
|
-
|
|
7459
|
-
Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builder. Re-run setup_ghl_mcp later with fresh Firebase fields to enable it.`;
|
|
7460
|
-
} else {
|
|
7461
|
-
workflowBuilderEnabled = true;
|
|
7462
|
-
}
|
|
7463
|
-
}
|
|
7464
|
-
writeCredentials({
|
|
7465
|
-
license_key: args.license_key.trim(),
|
|
7466
|
-
email: args.email.trim(),
|
|
7467
|
-
verified_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7468
|
-
ghl_api_key: args.ghl_api_key.trim(),
|
|
7469
|
-
ghl_location_id: args.ghl_location_id.trim(),
|
|
7470
|
-
ghl_company_id: args.ghl_company_id?.trim() || void 0,
|
|
7471
|
-
ghl_user_id: workflowBuilderEnabled ? args.ghl_user_id?.trim() : void 0,
|
|
7472
|
-
ghl_firebase_api_key: workflowBuilderEnabled ? args.ghl_firebase_api_key?.trim() : void 0,
|
|
7473
|
-
ghl_firebase_refresh_token: workflowBuilderEnabled ? args.ghl_firebase_refresh_token?.trim() : void 0
|
|
7474
|
-
});
|
|
7475
|
-
const toolCount = workflowBuilderEnabled ? "201" : "161";
|
|
7476
|
-
const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
|
|
7477
|
-
const wfTip = workflowBuilderEnabled ? "" : "\nTo enable Workflow Builder later (8 extra tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
|
|
7478
|
-
return {
|
|
7479
|
-
content: [{
|
|
7480
|
-
type: "text",
|
|
7481
|
-
text: [
|
|
7482
|
-
`Setup complete!`,
|
|
7483
|
-
``,
|
|
7484
|
-
`License: verified (installs ${lic.installs}).`,
|
|
7485
|
-
`GHL: connected to "${ghl.locationName}".`,
|
|
7486
|
-
wfLine,
|
|
7487
|
-
``,
|
|
7488
|
-
`Credentials saved to: ${credentialsPath()}`,
|
|
7489
|
-
``,
|
|
7490
|
-
`**Restart Claude (quit fully and reopen) to load all ${toolCount} tools.**`,
|
|
7491
|
-
``,
|
|
7492
|
-
`After restart, try: "List my GHL contacts" or "Show my pipelines".${wfTip}`,
|
|
7493
|
-
workflowBuilderNote
|
|
7494
|
-
].join("\n")
|
|
7495
|
-
}]
|
|
7496
|
-
};
|
|
7497
|
-
}
|
|
7498
|
-
);
|
|
7499
|
-
}
|
|
7500
|
-
function registerEnableWorkflowBuilderTool(server2) {
|
|
7501
|
-
server2.tool(
|
|
7502
|
-
"enable_workflow_builder",
|
|
7503
|
-
"Add Firebase credentials to an existing GHL Command install to unlock 30 additional tools across 6 modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder (10 tools), form builder (5 tools), pipeline builder (5 tools), and workflow cloning. Requires you've already run setup_ghl_mcp. Capture the three Firebase values from your GHL browser session \u2014 see elitedcs.com/ghl-mcp-firebase for step-by-step DevTools instructions. Tool count goes from 161 to 201 after the next Claude restart.",
|
|
7504
|
-
{
|
|
7505
|
-
ghl_user_id: import_zod47.z.string().min(10).describe("Firebase User ID (uid). DevTools \u2192 Application \u2192 IndexedDB \u2192 firebaseLocalStorageDb \u2192 firebaseLocalStorage \u2192 the value.uid field of the firebase:authUser row."),
|
|
7506
|
-
ghl_firebase_api_key: import_zod47.z.string().min(10).describe("Firebase API Key starting with 'AIza'. The string between 'firebase:authUser:' and ':[DEFAULT]' in the row's Key column."),
|
|
7507
|
-
ghl_firebase_refresh_token: import_zod47.z.string().min(10).describe("Firebase refresh token. value.stsTokenManager.refreshToken in the firebase:authUser row.")
|
|
7508
|
-
},
|
|
7509
|
-
async (args) => {
|
|
7510
|
-
const existing = readCredentials();
|
|
7511
|
-
if (!existing) {
|
|
7512
|
-
return {
|
|
7513
|
-
content: [{
|
|
7514
|
-
type: "text",
|
|
7515
|
-
text: "No existing credentials found at " + credentialsPath() + ".\n\nRun setup_ghl_mcp first to register your license and basic GHL credentials, then come back to this tool to add Workflow Builder."
|
|
7516
|
-
}],
|
|
7517
|
-
isError: true
|
|
7518
|
-
};
|
|
7519
|
-
}
|
|
7520
|
-
const fb = await validateFirebase(args.ghl_firebase_api_key.trim(), args.ghl_firebase_refresh_token.trim());
|
|
7521
|
-
if (!fb.ok) {
|
|
7522
|
-
return {
|
|
7523
|
-
content: [{
|
|
7524
|
-
type: "text",
|
|
7525
|
-
text: `Firebase credentials rejected: ${fb.error}
|
|
7526
|
-
|
|
7527
|
-
Common causes:
|
|
7528
|
-
- The refresh token has rotated (they rotate every few weeks). Re-extract from your GHL browser tab.
|
|
7529
|
-
- The Firebase API Key doesn't match the refresh token's project. Both must come from the SAME firebase:authUser row.
|
|
7530
|
-
|
|
7531
|
-
DevTools steps: https://elitedcs.com/ghl-mcp-firebase`
|
|
7532
|
-
}],
|
|
7533
|
-
isError: true
|
|
7534
|
-
};
|
|
7535
|
-
}
|
|
7536
|
-
writeCredentials({
|
|
7537
|
-
...existing,
|
|
7538
|
-
ghl_user_id: args.ghl_user_id.trim(),
|
|
7539
|
-
ghl_firebase_api_key: args.ghl_firebase_api_key.trim(),
|
|
7540
|
-
ghl_firebase_refresh_token: args.ghl_firebase_refresh_token.trim()
|
|
7541
|
-
});
|
|
7542
|
-
return {
|
|
7543
|
-
content: [{
|
|
7544
|
-
type: "text",
|
|
7545
|
-
text: [
|
|
7546
|
-
"Workflow Builder enabled!",
|
|
7547
|
-
"",
|
|
7548
|
-
"Firebase credentials verified and saved.",
|
|
7549
|
-
"",
|
|
7550
|
-
"**Restart Claude (quit fully and reopen) to load the workflow builder + funnel builder + pipeline builder + form builder + workflow cloner tools (201 total).**",
|
|
7551
|
-
"",
|
|
7552
|
-
'After restart, try: "List my workflows in full detail" or "Validate workflow <id>".',
|
|
7553
|
-
"",
|
|
7554
|
-
"Note: Firebase refresh tokens rotate every few weeks. If workflow tools stop working, re-run enable_workflow_builder with fresh values from a current GHL browser session."
|
|
7555
|
-
].join("\n")
|
|
7556
|
-
}]
|
|
7557
|
-
};
|
|
7558
|
-
}
|
|
7559
|
-
);
|
|
7560
|
-
}
|
|
7561
|
-
|
|
7562
7946
|
// src/tools/meta.ts
|
|
7563
7947
|
function registerMetaTools(server2, installedVersion) {
|
|
7564
7948
|
server2.tool(
|