@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/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.13.1",
35
- description: "GoHighLevel MCP Server for Claude. 201 tools \u2014 full CRM, automation, marketing control, and the only programmatic GHL workflow builder.",
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({ name: import_zod2.z.string(), apiKey: import_zod2.z.string() });
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
- firebase: import_zod2.z.object({
345
- apiKey: import_zod2.z.string(),
346
- refreshToken: import_zod2.z.string(),
347
- userId: import_zod2.z.string()
348
- }).optional()
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
- registerLocation(locationId2, name, apiKey2) {
459
- this.data.tokens[locationId2] = { name, apiKey: apiKey2 };
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.persistRefreshToken(data.refresh_token);
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. Renaming the title or deleting the template is not yet possible through the public API \u2014 do those in the GHL UI for now.",
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 switchChain = Promise.resolve();
5436
- function withSwitchLock(fn) {
5437
- const next = switchChain.then(fn, fn);
5438
- switchChain = next.catch(() => void 0);
5439
- return next;
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 registerLocationSwitcherTools(server2, client, builderClient, registry2, mcpVersion) {
5442
- const versionLine = mcpVersion ? `
5443
- MCP version: v${mcpVersion}` : "";
5444
- server2.tool(
5445
- "get_current_location",
5446
- "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.",
5447
- {},
5448
- async () => {
5449
- const locId = client.defaultLocationId || "NOT SET";
5450
- try {
5451
- if (client.defaultLocationId) {
5452
- const result = await client.get(`/locations/${client.defaultLocationId}`);
5453
- const loc = result.location ?? result;
5454
- const registeredCount = registry2 ? registry2.listLocations().length : 0;
5455
- return {
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
- "switch_location",
5481
- "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.",
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
- locationId: import_zod37.z.string().describe("The Location ID to switch to.")
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 ({ locationId: locationId2 }) => withSwitchLock(async () => {
5486
- const previousId = client.defaultLocationId;
5487
- const previousKeyPrefix = client.getApiKeyPrefix();
5488
- const previousApiKey = client.getApiKey();
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
- All tools now default to this location.`
5519
- }
5520
- ]
5521
- };
5522
- } catch (error) {
5523
- const message = error instanceof Error ? error.message : String(error);
5524
- client.defaultLocationId = previousId;
5525
- client.setApiKey(previousApiKey);
5526
- if (builderClient && previousBuilderApiKey !== void 0) {
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: import_zod37.z.string().describe("The GHL Location ID (from Settings > Business Profile)."),
5549
- name: import_zod37.z.string().describe("A friendly name for this sub-account (e.g. 'PNTracker', 'Med Spa Template')."),
5550
- apiKey: import_zod37.z.string().describe("The Private Integration API key for this sub-account (starts with 'pit-').")
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
- registry2.registerLocation(locationId2, confirmedName, apiKey2);
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 .ghl-tokens.json
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: import_zod37.z.string().describe("The Location ID to remove.")
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 their names and IDs. These are locations that switch_location can automatically authenticate to.",
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: "No locations registered. Use register_location to add sub-accounts."
6216
+ text: `No locations registered. Use register_location to add sub-accounts.${fbSection2}`
5633
6217
  }
5634
6218
  ]
5635
6219
  };
5636
6220
  }
5637
- const locations = registry2.listLocations();
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 = locations.map(
5640
- (loc) => `${loc.locationId === currentId ? "\u2192 " : " "}${loc.name} (${loc.locationId})`
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 (${locations.length} locations):
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: import_zod37.z.number().optional().describe("Max locations to return. Defaults to 20."),
5660
- skip: import_zod37.z.number().optional().describe("Number to skip for pagination.")
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 import_zod38 = require("zod");
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: import_zod38.z.array(import_zod38.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to tag."),
5716
- tags: import_zod38.z.array(import_zod38.z.string()).min(1, "At least one tag required.").describe("Tags to add to each contact.")
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: import_zod38.z.array(import_zod38.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs."),
5739
- tags: import_zod38.z.array(import_zod38.z.string()).min(1, "At least one tag required.").describe("Tags to remove from each contact.")
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: import_zod38.z.array(import_zod38.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
5761
- fields: import_zod38.z.record(import_zod38.z.unknown()).describe("Fields to set on each contact (e.g. {customField: {id: 'xxx', value: 'yyy'}}, {source: 'Import'}).")
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: import_zod38.z.array(import_zod38.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to enroll."),
5783
- workflowId: import_zod38.z.string().describe("The workflow ID to enroll contacts into.")
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: import_zod38.z.array(import_zod38.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to permanently delete."),
5805
- confirm: import_zod38.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
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 import_zod39 = require("zod");
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: import_zod39.z.string().optional().describe("Location ID to export. Uses default if not specified."),
5839
- includeContacts: import_zod39.z.boolean().optional().describe("Include contact list (first 100). Defaults to false for speed.")
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: import_zod39.z.string().describe("First Location ID."),
5968
- locationB: import_zod39.z.string().describe("Second Location ID.")
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 import_zod40 = require("zod");
6047
- var crypto2 = __toESM(require("crypto"));
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: import_zod40.z.string().describe("The workflow ID to clone."),
6056
- newName: import_zod40.z.string().describe("Name for the cloned workflow.")
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, crypto2.randomUUID());
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, crypto2.randomUUID());
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 import_zod41 = require("zod");
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: import_zod41.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."),
6173
- query: import_zod41.z.string().optional().describe("Free-text search across smart list names."),
6174
- limit: import_zod41.z.number().optional().describe("Max smart lists per page. Defaults to 20 on GHL's side."),
6175
- startAfter: import_zod41.z.string().optional().describe("Cursor for pagination \u2014 pass the last list's id from the previous page."),
6176
- locationId: import_zod41.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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: import_zod41.z.string().describe("The smart list ID (from list_smart_lists or a previous create_smart_list response)."),
6197
- locationId: import_zod41.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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: import_zod41.z.string().describe("Display name for the smart list."),
6214
- objectKey: import_zod41.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
6215
- filters: import_zod41.z.array(import_zod41.z.record(import_zod41.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."),
6216
- columns: import_zod41.z.array(import_zod41.z.record(import_zod41.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."),
6217
- pipelineIds: import_zod41.z.array(import_zod41.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs to restrict this smart list to. Empty array = all pipelines."),
6218
- defaultInPipelines: import_zod41.z.array(import_zod41.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
6219
- locationId: import_zod41.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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: import_zod41.z.string().describe("The smart list ID to update."),
6241
- name: import_zod41.z.string().optional().describe("New display name."),
6242
- filters: import_zod41.z.array(import_zod41.z.record(import_zod41.z.unknown())).optional().describe("Replace the filter array entirely. To add a filter, fetch the current list, append, and pass the new array."),
6243
- columns: import_zod41.z.array(import_zod41.z.record(import_zod41.z.unknown())).optional().describe("Replace the column array entirely."),
6244
- pipelineIds: import_zod41.z.array(import_zod41.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
6245
- defaultInPipelines: import_zod41.z.array(import_zod41.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
6246
- locationId: import_zod41.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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: import_zod41.z.string().describe("The smart list ID to delete."),
6272
- confirm: import_zod41.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
6273
- locationId: import_zod41.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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 import_zod42 = require("zod");
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: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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 import_zod43 = require("zod");
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: import_zod43.z.string().describe("ID of an email template (from create_email_template or list_email_templates) to use as the campaign body."),
6334
- name: import_zod43.z.string().optional().describe("Internal campaign name (shown in the campaigns list, not to recipients). Defaults to a GHL-generated name."),
6335
- subject: import_zod43.z.string().optional().describe("Email subject line recipients see."),
6336
- fromName: import_zod43.z.string().optional().describe("Sender display name."),
6337
- fromEmail: import_zod43.z.string().optional().describe("Sender email address. Must be a verified sending address in the location."),
6338
- isPlainText: import_zod43.z.boolean().optional().describe("Send as plain text instead of HTML. Defaults to false."),
6339
- enableResendToUnopened: import_zod43.z.boolean().optional().describe("Auto-resend to contacts who didn't open. Defaults to false."),
6340
- hasUtmTracking: import_zod43.z.boolean().optional().describe("Append UTM tracking params to links. Defaults to false."),
6341
- locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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 import_zod44 = require("zod");
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: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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: import_zod44.z.number().optional().describe("Max categories to return. Defaults to a large value (effectively all)."),
6417
- locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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: import_zod44.z.number().optional().describe("Max lessons to return. Defaults to a large value (effectively all)."),
6434
- locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
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 import_zod45 = require("zod");
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 = import_zod45.z.object({
6456
- templateName: import_zod45.z.string(),
6457
- templateVersion: import_zod45.z.string().optional(),
6458
- description: import_zod45.z.string().optional().default(""),
6459
- questionnaire: import_zod45.z.array(import_zod45.z.object({
6460
- id: import_zod45.z.string(),
6461
- question: import_zod45.z.string(),
6462
- type: import_zod45.z.string(),
6463
- required: import_zod45.z.boolean().optional(),
6464
- placeholder: import_zod45.z.string().optional()
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: import_zod45.z.record(import_zod45.z.unknown()).optional(),
6467
- tags: import_zod45.z.array(import_zod45.z.string()).optional(),
6468
- customFields: import_zod45.z.array(import_zod45.z.object({
6469
- name: import_zod45.z.string(),
6470
- dataType: import_zod45.z.string()
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: import_zod45.z.array(import_zod45.z.object({
6473
- name: import_zod45.z.string(),
6474
- stages: import_zod45.z.array(import_zod45.z.object({ position: import_zod45.z.number(), name: import_zod45.z.string() }))
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: import_zod45.z.array(import_zod45.z.object({
6477
- name: import_zod45.z.string(),
6478
- condition: import_zod45.z.string().optional(),
6479
- actions: import_zod45.z.array(import_zod45.z.record(import_zod45.z.unknown())).optional().default([])
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: import_zod45.z.array(import_zod45.z.object({
6482
- name: import_zod45.z.string(),
6483
- description: import_zod45.z.string().optional()
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: import_zod45.z.string().describe("Path to the template JSON file (from list_templates).")
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: import_zod45.z.string().describe("Path to the template JSON file."),
6588
- answers: import_zod45.z.record(import_zod45.z.unknown()).describe("Questionnaire answers keyed by question ID (e.g. {business_name: 'My Clinic', business_phone: '+15551234567', ...})."),
6589
- locationId: import_zod45.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
6590
- dryRun: import_zod45.z.boolean().optional().describe("If true, shows what would be created without actually creating anything. Defaults to false.")
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 import_zod46 = require("zod");
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: import_zod46.z.string().describe("The workflow ID to validate.")
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: "ID token refresh succeeded. Workflow builder tools are usable." };
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}. Re-capture Firebase values from GHL DevTools and re-run setup_ghl_mcp.` };
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: "Initialized but empty \u2014 register sub-accounts via register_location for cross-account switching." };
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(