@elitedcs/ghl-mcp 3.13.1 → 3.14.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.14.0",
35
+ description: "GoHighLevel MCP Server for Claude. 203 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
  *
@@ -5431,89 +5584,337 @@ ${text2}`);
5431
5584
  }
5432
5585
 
5433
5586
  // src/tools/location-switcher.ts
5587
+ var import_zod38 = require("zod");
5588
+
5589
+ // src/setup-tool.ts
5590
+ var os2 = __toESM(require("os"));
5591
+ var crypto2 = __toESM(require("crypto"));
5434
5592
  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;
5593
+ var LICENSE_API = "https://elitedcs.com/api/validate-license";
5594
+ var GHL_API = "https://services.leadconnectorhq.com";
5595
+ var FIREBASE_TOKEN_API = "https://securetoken.googleapis.com/v1/token";
5596
+ function deviceFingerprint() {
5597
+ const raw = `${os2.hostname()}:${os2.userInfo().username}:${os2.platform()}:${os2.arch()}`;
5598
+ return crypto2.createHash("sha256").update(raw).digest("hex").slice(0, 16);
5440
5599
  }
5441
- function registerLocationSwitcherTools(server2, client, builderClient, registry2, mcpVersion) {
5442
- const versionLine = mcpVersion ? `
5443
- MCP version: v${mcpVersion}` : "";
5600
+ async function validateLicense(email, licenseKey) {
5601
+ try {
5602
+ const res = await fetch(LICENSE_API, {
5603
+ method: "POST",
5604
+ headers: { "Content-Type": "application/json" },
5605
+ body: JSON.stringify({
5606
+ email: email.trim(),
5607
+ license_key: licenseKey.trim(),
5608
+ device_fingerprint: deviceFingerprint()
5609
+ }),
5610
+ signal: AbortSignal.timeout(1e4)
5611
+ });
5612
+ const data = await res.json().catch(() => ({}));
5613
+ if (res.ok && data.valid) {
5614
+ return { ok: true, installs: `${data.installs_used}/${data.installs_max}` };
5615
+ }
5616
+ return { ok: false, error: data.error || data.message || `License validation failed (HTTP ${res.status})` };
5617
+ } catch (err) {
5618
+ const msg = err instanceof Error ? err.message : String(err);
5619
+ return { ok: false, error: `Could not reach license server: ${msg}` };
5620
+ }
5621
+ }
5622
+ async function validateGhl(apiKey2, locationId2) {
5623
+ try {
5624
+ const res = await fetch(`${GHL_API}/locations/${locationId2}`, {
5625
+ headers: {
5626
+ Authorization: `Bearer ${apiKey2}`,
5627
+ Version: "2021-07-28",
5628
+ Accept: "application/json"
5629
+ },
5630
+ signal: AbortSignal.timeout(1e4)
5631
+ });
5632
+ if (res.status === 401) return { ok: false, error: "GHL API key is invalid (401)." };
5633
+ 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." };
5634
+ if (!res.ok) return { ok: false, error: `GHL returned HTTP ${res.status}.` };
5635
+ const data = await res.json().catch(() => ({}));
5636
+ const name = data?.location?.name || data?.name || "Unknown";
5637
+ return { ok: true, locationName: name };
5638
+ } catch (err) {
5639
+ const msg = err instanceof Error ? err.message : String(err);
5640
+ return { ok: false, error: `Could not reach GHL: ${msg}` };
5641
+ }
5642
+ }
5643
+ async function validateFirebase(firebaseKey, refreshToken) {
5644
+ try {
5645
+ const res = await fetch(`${FIREBASE_TOKEN_API}?key=${encodeURIComponent(firebaseKey)}`, {
5646
+ method: "POST",
5647
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
5648
+ body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,
5649
+ signal: AbortSignal.timeout(1e4)
5650
+ });
5651
+ if (!res.ok) return { ok: false, error: "Firebase credentials rejected. Re-extract them from your GHL browser tab." };
5652
+ return { ok: true };
5653
+ } catch (err) {
5654
+ const msg = err instanceof Error ? err.message : String(err);
5655
+ return { ok: false, error: `Could not reach Firebase: ${msg}` };
5656
+ }
5657
+ }
5658
+ function registerSetupTool(server2) {
5444
5659
  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;
5660
+ "setup_ghl_mcp",
5661
+ "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 203 tools (163 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
5662
+ {
5663
+ email: import_zod37.z.string().email().describe("Email used at purchase."),
5664
+ license_key: import_zod37.z.string().min(20).describe("License key from your purchase email."),
5665
+ 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."),
5666
+ 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."),
5667
+ ghl_company_id: import_zod37.z.string().optional().describe("(Agency only) Company ID for multi-location access."),
5668
+ ghl_user_id: import_zod37.z.string().optional().describe("(Workflow Builder, optional) Firebase User ID. See README for browser capture instructions."),
5669
+ ghl_firebase_api_key: import_zod37.z.string().optional().describe("(Workflow Builder, optional) Firebase API Key starting with 'AIza'."),
5670
+ ghl_firebase_refresh_token: import_zod37.z.string().optional().describe("(Workflow Builder, optional) Firebase refresh token starting with 'AMf-'.")
5671
+ },
5672
+ async (args) => {
5673
+ const lic = await validateLicense(args.email, args.license_key);
5674
+ if (!lic.ok) {
5675
+ return { content: [{ type: "text", text: `License check failed: ${lic.error}
5676
+
5677
+ Purchase a license at https://elitedcs.com/ghl-mcp-server or contact support.` }], isError: true };
5678
+ }
5679
+ const ghl = await validateGhl(args.ghl_api_key, args.ghl_location_id);
5680
+ if (!ghl.ok) {
5681
+ return { content: [{ type: "text", text: `GHL credential check failed: ${ghl.error}` }], isError: true };
5682
+ }
5683
+ const wantsWorkflowBuilder = args.ghl_user_id || args.ghl_firebase_api_key || args.ghl_firebase_refresh_token;
5684
+ let workflowBuilderEnabled = false;
5685
+ let workflowBuilderNote = "";
5686
+ if (wantsWorkflowBuilder) {
5687
+ if (!args.ghl_user_id || !args.ghl_firebase_api_key || !args.ghl_firebase_refresh_token) {
5455
5688
  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
- ]
5689
+ content: [{
5690
+ type: "text",
5691
+ 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."
5692
+ }],
5693
+ isError: true
5467
5694
  };
5468
5695
  }
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
- };
5696
+ const fb = await validateFirebase(args.ghl_firebase_api_key, args.ghl_firebase_refresh_token);
5697
+ if (!fb.ok) {
5698
+ workflowBuilderNote = `
5699
+
5700
+ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builder. Re-run setup_ghl_mcp later with fresh Firebase fields to enable it.`;
5701
+ } else {
5702
+ workflowBuilderEnabled = true;
5703
+ }
5476
5704
  }
5705
+ writeCredentials({
5706
+ license_key: args.license_key.trim(),
5707
+ email: args.email.trim(),
5708
+ verified_at: (/* @__PURE__ */ new Date()).toISOString(),
5709
+ ghl_api_key: args.ghl_api_key.trim(),
5710
+ ghl_location_id: args.ghl_location_id.trim(),
5711
+ ghl_company_id: args.ghl_company_id?.trim() || void 0,
5712
+ ghl_user_id: workflowBuilderEnabled ? args.ghl_user_id?.trim() : void 0,
5713
+ ghl_firebase_api_key: workflowBuilderEnabled ? args.ghl_firebase_api_key?.trim() : void 0,
5714
+ ghl_firebase_refresh_token: workflowBuilderEnabled ? args.ghl_firebase_refresh_token?.trim() : void 0
5715
+ });
5716
+ const toolCount = workflowBuilderEnabled ? "203" : "163";
5717
+ const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
5718
+ 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.";
5719
+ return {
5720
+ content: [{
5721
+ type: "text",
5722
+ text: [
5723
+ `Setup complete!`,
5724
+ ``,
5725
+ `License: verified (installs ${lic.installs}).`,
5726
+ `GHL: connected to "${ghl.locationName}".`,
5727
+ wfLine,
5728
+ ``,
5729
+ `Credentials saved to: ${credentialsPath()}`,
5730
+ ``,
5731
+ `**Restart Claude (quit fully and reopen) to load all ${toolCount} tools.**`,
5732
+ ``,
5733
+ `After restart, try: "List my GHL contacts" or "Show my pipelines".${wfTip}`,
5734
+ workflowBuilderNote
5735
+ ].join("\n")
5736
+ }]
5737
+ };
5477
5738
  }
5478
5739
  );
5740
+ }
5741
+ function registerEnableWorkflowBuilderTool(server2) {
5479
5742
  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.",
5743
+ "enable_workflow_builder",
5744
+ "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.",
5482
5745
  {
5483
- locationId: import_zod37.z.string().describe("The Location ID to switch to.")
5746
+ 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."),
5747
+ 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."),
5748
+ ghl_firebase_refresh_token: import_zod37.z.string().min(10).describe("Firebase refresh token. value.stsTokenManager.refreshToken in the firebase:authUser row.")
5484
5749
  },
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" : ""}`;
5750
+ async (args) => {
5751
+ const existing = readCredentials();
5752
+ if (!existing) {
5753
+ return {
5754
+ content: [{
5755
+ type: "text",
5756
+ 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."
5757
+ }],
5758
+ isError: true
5759
+ };
5760
+ }
5761
+ const fb = await validateFirebase(args.ghl_firebase_api_key.trim(), args.ghl_firebase_refresh_token.trim());
5762
+ if (!fb.ok) {
5763
+ return {
5764
+ content: [{
5765
+ type: "text",
5766
+ text: `Firebase credentials rejected: ${fb.error}
5767
+
5768
+ Common causes:
5769
+ - The refresh token has rotated (they rotate every few weeks). Re-extract from your GHL browser tab.
5770
+ - The Firebase API Key doesn't match the refresh token's project. Both must come from the SAME firebase:authUser row.
5771
+
5772
+ DevTools steps: https://elitedcs.com/ghl-mcp-firebase`
5773
+ }],
5774
+ isError: true
5775
+ };
5776
+ }
5777
+ writeCredentials({
5778
+ ...existing,
5779
+ ghl_user_id: args.ghl_user_id.trim(),
5780
+ ghl_firebase_api_key: args.ghl_firebase_api_key.trim(),
5781
+ ghl_firebase_refresh_token: args.ghl_firebase_refresh_token.trim()
5782
+ });
5783
+ return {
5784
+ content: [{
5785
+ type: "text",
5786
+ text: [
5787
+ "Workflow Builder enabled!",
5788
+ "",
5789
+ "Firebase credentials verified and saved.",
5790
+ "",
5791
+ "**Restart Claude (quit fully and reopen) to load the workflow builder + funnel builder + pipeline builder + form builder + workflow cloner tools (203 total).**",
5792
+ "",
5793
+ 'After restart, try: "List my workflows in full detail" or "Validate workflow <id>".',
5794
+ "",
5795
+ "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."
5796
+ ].join("\n")
5797
+ }]
5798
+ };
5799
+ }
5800
+ );
5801
+ }
5802
+
5803
+ // src/tools/location-switcher.ts
5804
+ function routeFirebaseForCompany(builderClient, registry2, companyId) {
5805
+ if (!builderClient) return "";
5806
+ if (!companyId) {
5807
+ return "\nFirebase: could not determine this location's company \u2014 workflow-builder auth left unchanged.";
5808
+ }
5809
+ const cfg = registry2?.getCompanyFirebase(companyId);
5810
+ if (cfg) {
5811
+ builderClient.applyCompanyFirebase({
5812
+ apiKey: cfg.apiKey,
5813
+ refreshToken: cfg.refreshToken,
5814
+ userId: cfg.userId,
5815
+ companyId
5816
+ });
5817
+ return `
5818
+ Firebase: using ${cfg.name || `company ${companyId}`} credentials \u2014 workflow builder enabled here.`;
5819
+ }
5820
+ builderClient.resetToHomeFirebase();
5821
+ const homeCompanyId = builderClient.getCurrentCompanyId();
5822
+ if (homeCompanyId === void 0) {
5823
+ 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.";
5824
+ }
5825
+ if (homeCompanyId === companyId) {
5826
+ return "\nFirebase: home company \u2014 workflow builder enabled.";
5827
+ }
5828
+ return `
5829
+ 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.`;
5830
+ }
5831
+ var switchChain = Promise.resolve();
5832
+ function withSwitchLock(fn) {
5833
+ const next = switchChain.then(fn, fn);
5834
+ switchChain = next.catch(() => void 0);
5835
+ return next;
5836
+ }
5837
+ function registerLocationSwitcherTools(server2, client, builderClient, registry2, mcpVersion) {
5838
+ const versionLine = mcpVersion ? `
5839
+ MCP version: v${mcpVersion}` : "";
5840
+ server2.tool(
5841
+ "get_current_location",
5842
+ "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.",
5843
+ {},
5844
+ async () => {
5845
+ const locId = client.defaultLocationId || "NOT SET";
5846
+ try {
5847
+ if (client.defaultLocationId) {
5848
+ const result = await client.get(`/locations/${client.defaultLocationId}`);
5849
+ const loc = result.location ?? result;
5850
+ const registeredCount = registry2 ? registry2.listLocations().length : 0;
5851
+ return {
5852
+ content: [
5853
+ {
5854
+ type: "text",
5855
+ text: `Current location: ${loc?.name || "Unknown"}
5856
+ ID: ${client.defaultLocationId}
5857
+ API Key: ${client.getApiKeyPrefix()}
5858
+ Address: ${loc?.address || "N/A"}
5859
+ Email: ${loc?.email || "N/A"}
5860
+ Token registry: ${registeredCount} location(s) registered${versionLine}`
5861
+ }
5862
+ ]
5863
+ };
5864
+ }
5865
+ return {
5866
+ content: [{ type: "text", text: `No default location set. Pass locationId to tools or use switch_location.${versionLine}` }]
5867
+ };
5868
+ } catch (error) {
5869
+ return {
5870
+ content: [{ type: "text", text: `Current location ID: ${locId} (could not fetch details \u2014 API key may not have access)${versionLine}` }]
5871
+ };
5872
+ }
5873
+ }
5874
+ );
5875
+ server2.tool(
5876
+ "switch_location",
5877
+ "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.",
5878
+ {
5879
+ locationId: import_zod38.z.string().describe("The Location ID to switch to.")
5880
+ },
5881
+ async ({ locationId: locationId2 }) => withSwitchLock(async () => {
5882
+ const previousId = client.defaultLocationId;
5883
+ const previousKeyPrefix = client.getApiKeyPrefix();
5884
+ const previousApiKey = client.getApiKey();
5885
+ const previousBuilderApiKey = builderClient?.getApiKey();
5886
+ try {
5887
+ let keySwapped = false;
5888
+ if (registry2) {
5889
+ const token = registry2.getToken(locationId2);
5890
+ if (token) {
5891
+ client.setApiKey(token.apiKey);
5892
+ if (builderClient) {
5893
+ builderClient.setApiKey(token.apiKey);
5894
+ }
5895
+ keySwapped = true;
5896
+ }
5897
+ }
5898
+ const result = await client.get(`/locations/${locationId2}`);
5899
+ const loc = result.location ?? result;
5900
+ const name = loc?.name || "Unknown";
5901
+ client.defaultLocationId = locationId2;
5902
+ if (builderClient) {
5903
+ builderClient.locationId = locationId2;
5904
+ }
5905
+ const targetCompanyId = loc?.companyId || registry2?.getToken(locationId2)?.companyId;
5906
+ if (targetCompanyId && registry2?.getToken(locationId2)) {
5907
+ registry2.setLocationCompanyId(locationId2, targetCompanyId);
5908
+ }
5909
+ const firebaseStatus = routeFirebaseForCompany(builderClient, registry2, targetCompanyId);
5910
+ 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
5911
  return {
5511
5912
  content: [
5512
5913
  {
5513
5914
  type: "text",
5514
5915
  text: `Switched to: ${name} (${locationId2})
5515
5916
  Previous: ${previousId || "none"}
5516
- ${keyStatus}
5917
+ ${keyStatus}${firebaseStatus}
5517
5918
 
5518
5919
  All tools now default to this location.`
5519
5920
  }
@@ -5545,9 +5946,9 @@ Still on: ${previousId || "none"}${hint}` }],
5545
5946
  "register_location",
5546
5947
  "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
5948
  {
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-').")
5949
+ locationId: import_zod38.z.string().describe("The GHL Location ID (from Settings > Business Profile)."),
5950
+ name: import_zod38.z.string().describe("A friendly name for this sub-account (e.g. 'PNTracker', 'Med Spa Template')."),
5951
+ apiKey: import_zod38.z.string().describe("The Private Integration API key for this sub-account (starts with 'pit-').")
5551
5952
  },
5552
5953
  async ({ locationId: locationId2, name, apiKey: apiKey2 }) => {
5553
5954
  if (!registry2) {
@@ -5561,14 +5962,21 @@ Still on: ${previousId || "none"}${hint}` }],
5561
5962
  const result = await testClient.get(`/locations/${locationId2}`);
5562
5963
  const loc = result.location ?? result;
5563
5964
  const confirmedName = loc?.name || name;
5564
- registry2.registerLocation(locationId2, confirmedName, apiKey2);
5965
+ const companyId = loc?.companyId;
5966
+ registry2.registerLocation(locationId2, confirmedName, apiKey2, companyId);
5967
+ let fbLine = "";
5968
+ if (companyId) {
5969
+ fbLine = registry2.getCompanyFirebase(companyId) ? `
5970
+ Company: ${companyId} (Firebase already registered \u2014 workflow builder will work here).` : `
5971
+ Company: ${companyId}. To use the workflow builder in this account, run register_company_firebase for this company (Firebase is company-scoped).`;
5972
+ }
5565
5973
  return {
5566
5974
  content: [
5567
5975
  {
5568
5976
  type: "text",
5569
5977
  text: `Registered: ${confirmedName} (${locationId2})
5570
5978
  API Key: ${apiKey2.substring(0, 12)}...
5571
- Saved to .ghl-tokens.json
5979
+ Saved to the token registry.${fbLine}
5572
5980
 
5573
5981
  You can now use switch_location to switch to this sub-account.`
5574
5982
  }
@@ -5597,7 +6005,7 @@ The API key could not access location ${locationId2}. Make sure:
5597
6005
  "unregister_location",
5598
6006
  "Remove a GHL sub-account from the token registry.",
5599
6007
  {
5600
- locationId: import_zod37.z.string().describe("The Location ID to remove.")
6008
+ locationId: import_zod38.z.string().describe("The Location ID to remove.")
5601
6009
  },
5602
6010
  async ({ locationId: locationId2 }) => {
5603
6011
  if (!registry2) {
@@ -5619,34 +6027,146 @@ The API key could not access location ${locationId2}. Make sure:
5619
6027
  };
5620
6028
  }
5621
6029
  );
6030
+ server2.tool(
6031
+ "register_company_firebase",
6032
+ "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.",
6033
+ {
6034
+ 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."),
6035
+ name: import_zod38.z.string().describe("Friendly name for this client/company (e.g. 'Nathan \u2014 Acme Health')."),
6036
+ 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."),
6037
+ 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."),
6038
+ 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."),
6039
+ 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.")
6040
+ },
6041
+ async (args) => {
6042
+ if (!registry2) {
6043
+ return {
6044
+ content: [{ type: "text", text: "Token registry not available." }],
6045
+ isError: true
6046
+ };
6047
+ }
6048
+ const apiKey2 = args.ghl_firebase_api_key?.trim() || process.env.GHL_FIREBASE_API_KEY;
6049
+ if (!apiKey2) {
6050
+ return {
6051
+ 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)." }],
6052
+ isError: true
6053
+ };
6054
+ }
6055
+ const refreshToken = args.ghl_firebase_refresh_token.trim();
6056
+ const userId = args.ghl_user_id.trim();
6057
+ const fb = await validateFirebase(apiKey2, refreshToken);
6058
+ if (!fb.ok) {
6059
+ return {
6060
+ content: [{ type: "text", text: `Firebase credentials rejected: ${fb.error}
6061
+
6062
+ Capture fresh values from a browser session logged into THIS company's GHL. Steps: https://elitedcs.com/ghl-mcp-firebase` }],
6063
+ isError: true
6064
+ };
6065
+ }
6066
+ registry2.setCompanyFirebase(args.companyId, { apiKey: apiKey2, refreshToken, userId, name: args.name });
6067
+ let testLine = "";
6068
+ if (args.test_location_id) {
6069
+ const token = registry2.getToken(args.test_location_id);
6070
+ if (!token) {
6071
+ testLine = `
6072
+
6073
+ Skipped end-to-end test: location ${args.test_location_id} isn't registered. Run register_location for it, then switch_location to confirm.`;
6074
+ } else {
6075
+ const probe = new WorkflowBuilderClient({
6076
+ firebaseApiKey: apiKey2,
6077
+ refreshToken,
6078
+ apiKey: token.apiKey,
6079
+ locationId: args.test_location_id,
6080
+ userId,
6081
+ companyId: args.companyId,
6082
+ registry: null
6083
+ });
6084
+ try {
6085
+ await probe.listWorkflows(1, 0);
6086
+ testLine = `
6087
+
6088
+ Verified: the workflow-builder API responded for "${token.name}" (${args.test_location_id}). These credentials work for this company.`;
6089
+ } catch (error) {
6090
+ const message = error instanceof Error ? error.message : String(error);
6091
+ testLine = `
6092
+
6093
+ WARNING: saved, but the end-to-end test FAILED for ${args.test_location_id}:
6094
+ ${message}
6095
+
6096
+ 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.`;
6097
+ }
6098
+ }
6099
+ }
6100
+ return {
6101
+ content: [{ type: "text", text: `Registered Firebase for ${args.name} (company ${args.companyId}).${testLine}
6102
+
6103
+ Now run switch_location to any of this company's sub-accounts \u2014 the workflow builder will authenticate against it automatically.` }]
6104
+ };
6105
+ }
6106
+ );
6107
+ server2.tool(
6108
+ "unregister_company_firebase",
6109
+ "Remove a company's Firebase credentials from the registry. Workflow-builder tools will stop working for that company's sub-accounts until re-registered.",
6110
+ {
6111
+ companyId: import_zod38.z.string().describe("The company ID to remove Firebase credentials for.")
6112
+ },
6113
+ async ({ companyId }) => {
6114
+ if (!registry2) {
6115
+ return {
6116
+ content: [{ type: "text", text: "Token registry not available." }],
6117
+ isError: true
6118
+ };
6119
+ }
6120
+ const removed = registry2.removeCompanyFirebase(companyId);
6121
+ return {
6122
+ content: [{ type: "text", text: removed ? `Removed Firebase credentials for company ${companyId}.` : `No Firebase credentials were registered for company ${companyId}.` }],
6123
+ isError: !removed
6124
+ };
6125
+ }
6126
+ );
5622
6127
  server2.tool(
5623
6128
  "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.",
6129
+ "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
6130
  {},
5626
6131
  async () => {
6132
+ const companies = registry2?.listCompanyFirebases() ?? [];
6133
+ const companiesWithFirebase = new Set(companies.map((c) => c.companyId));
5627
6134
  if (!registry2 || !registry2.hasTokens()) {
6135
+ const fbSection2 = companies.length ? `
6136
+
6137
+ Company Firebase credentials (${companies.length}):
6138
+ ${companies.map((c) => ` ${c.name || c.companyId} (${c.companyId})`).join("\n")}` : "";
5628
6139
  return {
5629
6140
  content: [
5630
6141
  {
5631
6142
  type: "text",
5632
- text: "No locations registered. Use register_location to add sub-accounts."
6143
+ text: `No locations registered. Use register_location to add sub-accounts.${fbSection2}`
5633
6144
  }
5634
6145
  ]
5635
6146
  };
5636
6147
  }
5637
- const locations = registry2.listLocations();
6148
+ const tokens = registry2.listLocations().map((loc) => ({
6149
+ ...loc,
6150
+ companyId: registry2.getToken(loc.locationId)?.companyId
6151
+ }));
5638
6152
  const currentId = client.defaultLocationId;
5639
- const lines = locations.map(
5640
- (loc) => `${loc.locationId === currentId ? "\u2192 " : " "}${loc.name} (${loc.locationId})`
5641
- );
6153
+ const lines = tokens.map((loc) => {
6154
+ const marker = loc.locationId === currentId ? "\u2192 " : " ";
6155
+ const company = loc.companyId ? ` [company ${loc.companyId}${companiesWithFirebase.has(loc.companyId) ? ", Firebase \u2713" : ", no Firebase"}]` : "";
6156
+ return `${marker}${loc.name} (${loc.locationId})${company}`;
6157
+ });
6158
+ const fbSection = companies.length ? `
6159
+
6160
+ Company Firebase credentials (${companies.length}) \u2014 workflow builder works in these companies' sub-accounts:
6161
+ ${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
6162
  return {
5643
6163
  content: [
5644
6164
  {
5645
6165
  type: "text",
5646
- text: `Token Registry (${locations.length} locations):
6166
+ text: `Token Registry (${tokens.length} locations):
5647
6167
  ${lines.join("\n")}
5648
6168
 
5649
- \u2192 = currently active`
6169
+ \u2192 = currently active${fbSection}`
5650
6170
  }
5651
6171
  ]
5652
6172
  };
@@ -5656,8 +6176,8 @@ ${lines.join("\n")}
5656
6176
  "list_available_locations",
5657
6177
  "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
6178
  {
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.")
6179
+ limit: import_zod38.z.number().optional().describe("Max locations to return. Defaults to 20."),
6180
+ skip: import_zod38.z.number().optional().describe("Number to skip for pagination.")
5661
6181
  },
5662
6182
  async ({ limit, skip }) => {
5663
6183
  try {
@@ -5700,7 +6220,7 @@ ${lines.join("\n")}
5700
6220
  }
5701
6221
 
5702
6222
  // src/tools/bulk-operations.ts
5703
- var import_zod38 = require("zod");
6223
+ var import_zod39 = require("zod");
5704
6224
  function delay(ms) {
5705
6225
  return new Promise((resolve5) => setTimeout(resolve5, ms));
5706
6226
  }
@@ -5712,8 +6232,8 @@ function registerBulkOperationTools(server2, client) {
5712
6232
  "bulk_add_tags",
5713
6233
  "Add tags to multiple contacts at once. Rate-limited to avoid API throttling. Returns a summary of successes and failures.",
5714
6234
  {
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.")
6235
+ contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to tag."),
6236
+ tags: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one tag required.").describe("Tags to add to each contact.")
5717
6237
  },
5718
6238
  async ({ contactIds, tags }) => {
5719
6239
  const results = { success: 0, failed: 0, errors: [] };
@@ -5735,8 +6255,8 @@ function registerBulkOperationTools(server2, client) {
5735
6255
  "bulk_remove_tags",
5736
6256
  "Remove tags from multiple contacts at once. Rate-limited.",
5737
6257
  {
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.")
6258
+ contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs."),
6259
+ tags: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one tag required.").describe("Tags to remove from each contact.")
5740
6260
  },
5741
6261
  async ({ contactIds, tags }) => {
5742
6262
  const results = { success: 0, failed: 0, errors: [] };
@@ -5757,8 +6277,8 @@ function registerBulkOperationTools(server2, client) {
5757
6277
  "bulk_update_contacts",
5758
6278
  "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
6279
  {
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'}).")
6280
+ contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to update."),
6281
+ 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
6282
  },
5763
6283
  async ({ contactIds, fields }) => {
5764
6284
  const results = { success: 0, failed: 0, errors: [] };
@@ -5779,8 +6299,8 @@ function registerBulkOperationTools(server2, client) {
5779
6299
  "bulk_add_to_workflow",
5780
6300
  "Enroll multiple contacts into a workflow at once. Rate-limited.",
5781
6301
  {
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.")
6302
+ contactIds: import_zod39.z.array(import_zod39.z.string()).min(1, "At least one contact ID required.").describe("Array of contact IDs to enroll."),
6303
+ workflowId: import_zod39.z.string().describe("The workflow ID to enroll contacts into.")
5784
6304
  },
5785
6305
  async ({ contactIds, workflowId }) => {
5786
6306
  const results = { success: 0, failed: 0, errors: [] };
@@ -5801,8 +6321,8 @@ function registerBulkOperationTools(server2, client) {
5801
6321
  "bulk_delete_contacts",
5802
6322
  "Delete multiple contacts at once. IRREVERSIBLE. Rate-limited. Use with extreme caution.",
5803
6323
  {
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.")
6324
+ 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."),
6325
+ confirm: import_zod39.z.literal("DELETE").describe("Must pass the string 'DELETE' to confirm. This is a safety check.")
5806
6326
  },
5807
6327
  async ({ contactIds, confirm }) => {
5808
6328
  if (confirm !== "DELETE") {
@@ -5825,7 +6345,7 @@ function registerBulkOperationTools(server2, client) {
5825
6345
  }
5826
6346
 
5827
6347
  // src/tools/account-export.ts
5828
- var import_zod39 = require("zod");
6348
+ var import_zod40 = require("zod");
5829
6349
  function delay2(ms) {
5830
6350
  return new Promise((resolve5) => setTimeout(resolve5, ms));
5831
6351
  }
@@ -5835,8 +6355,8 @@ function registerAccountExportTools(server2, client) {
5835
6355
  "export_account",
5836
6356
  "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
6357
  {
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.")
6358
+ locationId: import_zod40.z.string().optional().describe("Location ID to export. Uses default if not specified."),
6359
+ includeContacts: import_zod40.z.boolean().optional().describe("Include contact list (first 100). Defaults to false for speed.")
5840
6360
  },
5841
6361
  async ({ locationId: locationId2, includeContacts }) => {
5842
6362
  try {
@@ -5964,8 +6484,8 @@ function registerAccountExportTools(server2, client) {
5964
6484
  "compare_locations",
5965
6485
  "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
6486
  {
5967
- locationA: import_zod39.z.string().describe("First Location ID."),
5968
- locationB: import_zod39.z.string().describe("Second Location ID.")
6487
+ locationA: import_zod40.z.string().describe("First Location ID."),
6488
+ locationB: import_zod40.z.string().describe("Second Location ID.")
5969
6489
  },
5970
6490
  async ({ locationA, locationB }) => {
5971
6491
  try {
@@ -6043,8 +6563,8 @@ function registerAccountExportTools(server2, client) {
6043
6563
  }
6044
6564
 
6045
6565
  // src/tools/workflow-cloner.ts
6046
- var import_zod40 = require("zod");
6047
- var crypto2 = __toESM(require("crypto"));
6566
+ var import_zod41 = require("zod");
6567
+ var crypto3 = __toESM(require("crypto"));
6048
6568
  function registerWorkflowClonerTools(server2, builderClient) {
6049
6569
  const client = builderClient;
6050
6570
  if (!client) return;
@@ -6052,8 +6572,8 @@ function registerWorkflowClonerTools(server2, builderClient) {
6052
6572
  "clone_workflow",
6053
6573
  "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
6574
  {
6055
- sourceWorkflowId: import_zod40.z.string().describe("The workflow ID to clone."),
6056
- newName: import_zod40.z.string().describe("Name for the cloned workflow.")
6575
+ sourceWorkflowId: import_zod41.z.string().describe("The workflow ID to clone."),
6576
+ newName: import_zod41.z.string().describe("Name for the cloned workflow.")
6057
6577
  },
6058
6578
  async ({ sourceWorkflowId, newName }) => {
6059
6579
  try {
@@ -6063,12 +6583,12 @@ function registerWorkflowClonerTools(server2, builderClient) {
6063
6583
  const triggers = source.triggers || [];
6064
6584
  for (const action of actions) {
6065
6585
  if (action.id) {
6066
- idMap.set(action.id, crypto2.randomUUID());
6586
+ idMap.set(action.id, crypto3.randomUUID());
6067
6587
  }
6068
6588
  }
6069
6589
  for (const trigger of triggers) {
6070
6590
  if (trigger.id) {
6071
- idMap.set(trigger.id, crypto2.randomUUID());
6591
+ idMap.set(trigger.id, crypto3.randomUUID());
6072
6592
  }
6073
6593
  }
6074
6594
  const remap = (id) => {
@@ -6142,7 +6662,7 @@ function registerWorkflowClonerTools(server2, builderClient) {
6142
6662
  }
6143
6663
 
6144
6664
  // src/tools/smart-lists.ts
6145
- var import_zod41 = require("zod");
6665
+ var import_zod42 = require("zod");
6146
6666
  var SMARTLIST_BASE = "https://backend.leadconnectorhq.com/lists/dynamic";
6147
6667
  var OBJECT_KEYS = ["contacts", "opportunity"];
6148
6668
  function registerSmartListTools(server2, builderClient) {
@@ -6169,11 +6689,11 @@ ${text2}`);
6169
6689
  "list_smart_lists",
6170
6690
  "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
6691
  {
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.")
6692
+ 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."),
6693
+ query: import_zod42.z.string().optional().describe("Free-text search across smart list names."),
6694
+ limit: import_zod42.z.number().optional().describe("Max smart lists per page. Defaults to 20 on GHL's side."),
6695
+ startAfter: import_zod42.z.string().optional().describe("Cursor for pagination \u2014 pass the last list's id from the previous page."),
6696
+ locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6177
6697
  },
6178
6698
  async ({ objectKey, query, limit, startAfter, locationId: locationId2 }) => {
6179
6699
  try {
@@ -6193,8 +6713,8 @@ ${text2}`);
6193
6713
  "get_smart_list",
6194
6714
  "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
6715
  {
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.")
6716
+ listId: import_zod42.z.string().describe("The smart list ID (from list_smart_lists or a previous create_smart_list response)."),
6717
+ locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6198
6718
  },
6199
6719
  async ({ listId, locationId: locationId2 }) => {
6200
6720
  try {
@@ -6210,13 +6730,13 @@ ${text2}`);
6210
6730
  "create_smart_list",
6211
6731
  "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
6732
  {
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.")
6733
+ name: import_zod42.z.string().describe("Display name for the smart list."),
6734
+ objectKey: import_zod42.z.enum(OBJECT_KEYS).describe("Object type the list segments over. 'contacts' or 'opportunity'."),
6735
+ 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."),
6736
+ 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."),
6737
+ 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."),
6738
+ defaultInPipelines: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity objectKey only) Pipeline IDs where this list is the default view."),
6739
+ locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6220
6740
  },
6221
6741
  async ({ name, objectKey, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
6222
6742
  try {
@@ -6237,13 +6757,13 @@ ${text2}`);
6237
6757
  "update_smart_list",
6238
6758
  "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
6759
  {
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.")
6760
+ listId: import_zod42.z.string().describe("The smart list ID to update."),
6761
+ name: import_zod42.z.string().optional().describe("New display name."),
6762
+ 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."),
6763
+ columns: import_zod42.z.array(import_zod42.z.record(import_zod42.z.unknown())).optional().describe("Replace the column array entirely."),
6764
+ pipelineIds: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity only) Update the pipeline scope."),
6765
+ defaultInPipelines: import_zod42.z.array(import_zod42.z.string()).optional().describe("(opportunity only) Update the default-in-pipelines list."),
6766
+ locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6247
6767
  },
6248
6768
  async ({ listId, name, filters, columns, pipelineIds, defaultInPipelines, locationId: locationId2 }) => {
6249
6769
  try {
@@ -6268,9 +6788,9 @@ ${text2}`);
6268
6788
  "delete_smart_list",
6269
6789
  "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
6790
  {
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.")
6791
+ listId: import_zod42.z.string().describe("The smart list ID to delete."),
6792
+ confirm: import_zod42.z.literal("DELETE").describe("Must pass 'DELETE' to confirm this destructive action."),
6793
+ locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6274
6794
  },
6275
6795
  async ({ listId, locationId: locationId2 }) => {
6276
6796
  try {
@@ -6285,7 +6805,7 @@ ${text2}`);
6285
6805
  }
6286
6806
 
6287
6807
  // src/tools/reputation.ts
6288
- var import_zod42 = require("zod");
6808
+ var import_zod43 = require("zod");
6289
6809
  var REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
6290
6810
  function registerReputationTools(server2, builderClient) {
6291
6811
  const client = builderClient;
@@ -6306,7 +6826,7 @@ ${text2}`);
6306
6826
  "get_review_link_list",
6307
6827
  "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
6828
  {
6309
- locationId: import_zod42.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6829
+ locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6310
6830
  },
6311
6831
  async ({ locationId: locationId2 }) => {
6312
6832
  try {
@@ -6321,7 +6841,7 @@ ${text2}`);
6321
6841
  }
6322
6842
 
6323
6843
  // src/tools/email-campaigns.ts
6324
- var import_zod43 = require("zod");
6844
+ var import_zod44 = require("zod");
6325
6845
  var SVC_BASE = "https://services.leadconnectorhq.com";
6326
6846
  function registerEmailCampaignTools(server2, builderClient) {
6327
6847
  const client = builderClient;
@@ -6330,15 +6850,15 @@ function registerEmailCampaignTools(server2, builderClient) {
6330
6850
  "create_email_campaign",
6331
6851
  "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
6852
  {
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.")
6853
+ 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."),
6854
+ name: import_zod44.z.string().optional().describe("Internal campaign name (shown in the campaigns list, not to recipients). Defaults to a GHL-generated name."),
6855
+ subject: import_zod44.z.string().optional().describe("Email subject line recipients see."),
6856
+ fromName: import_zod44.z.string().optional().describe("Sender display name."),
6857
+ fromEmail: import_zod44.z.string().optional().describe("Sender email address. Must be a verified sending address in the location."),
6858
+ isPlainText: import_zod44.z.boolean().optional().describe("Send as plain text instead of HTML. Defaults to false."),
6859
+ enableResendToUnopened: import_zod44.z.boolean().optional().describe("Auto-resend to contacts who didn't open. Defaults to false."),
6860
+ hasUtmTracking: import_zod44.z.boolean().optional().describe("Append UTM tracking params to links. Defaults to false."),
6861
+ locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6342
6862
  },
6343
6863
  async ({ templateId, name, subject, fromName, fromEmail, isPlainText, enableResendToUnopened, hasUtmTracking, locationId: locationId2 }) => {
6344
6864
  try {
@@ -6376,7 +6896,7 @@ ${text2}`);
6376
6896
  }
6377
6897
 
6378
6898
  // src/tools/memberships.ts
6379
- var import_zod44 = require("zod");
6899
+ var import_zod45 = require("zod");
6380
6900
  var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
6381
6901
  function registerMembershipTools(server2, builderClient) {
6382
6902
  const client = builderClient;
@@ -6397,7 +6917,7 @@ ${text2}`);
6397
6917
  "list_membership_offers",
6398
6918
  "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
6919
  {
6400
- locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6920
+ locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6401
6921
  },
6402
6922
  async ({ locationId: locationId2 }) => {
6403
6923
  try {
@@ -6413,8 +6933,8 @@ ${text2}`);
6413
6933
  "list_membership_categories",
6414
6934
  "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
6935
  {
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.")
6936
+ limit: import_zod45.z.number().optional().describe("Max categories to return. Defaults to a large value (effectively all)."),
6937
+ locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6418
6938
  },
6419
6939
  async ({ limit, locationId: locationId2 }) => {
6420
6940
  try {
@@ -6430,8 +6950,8 @@ ${text2}`);
6430
6950
  "list_membership_lessons",
6431
6951
  "List all membership/course lessons in a location. Use the returned ids with the lesson_completed / lesson_started trigger conditions. READ-ONLY.",
6432
6952
  {
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.")
6953
+ limit: import_zod45.z.number().optional().describe("Max lessons to return. Defaults to a large value (effectively all)."),
6954
+ locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
6435
6955
  },
6436
6956
  async ({ limit, locationId: locationId2 }) => {
6437
6957
  try {
@@ -6446,41 +6966,41 @@ ${text2}`);
6446
6966
  }
6447
6967
 
6448
6968
  // src/tools/template-deployer.ts
6449
- var import_zod45 = require("zod");
6969
+ var import_zod46 = require("zod");
6450
6970
  var fs4 = __toESM(require("fs"));
6451
6971
  var path4 = __toESM(require("path"));
6452
6972
  function delay3(ms) {
6453
6973
  return new Promise((resolve5) => setTimeout(resolve5, ms));
6454
6974
  }
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()
6975
+ var TemplateSchema = import_zod46.z.object({
6976
+ templateName: import_zod46.z.string(),
6977
+ templateVersion: import_zod46.z.string().optional(),
6978
+ description: import_zod46.z.string().optional().default(""),
6979
+ questionnaire: import_zod46.z.array(import_zod46.z.object({
6980
+ id: import_zod46.z.string(),
6981
+ question: import_zod46.z.string(),
6982
+ type: import_zod46.z.string(),
6983
+ required: import_zod46.z.boolean().optional(),
6984
+ placeholder: import_zod46.z.string().optional()
6465
6985
  })).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()
6986
+ location: import_zod46.z.record(import_zod46.z.unknown()).optional(),
6987
+ tags: import_zod46.z.array(import_zod46.z.string()).optional(),
6988
+ customFields: import_zod46.z.array(import_zod46.z.object({
6989
+ name: import_zod46.z.string(),
6990
+ dataType: import_zod46.z.string()
6471
6991
  })).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() }))
6992
+ pipelines: import_zod46.z.array(import_zod46.z.object({
6993
+ name: import_zod46.z.string(),
6994
+ stages: import_zod46.z.array(import_zod46.z.object({ position: import_zod46.z.number(), name: import_zod46.z.string() }))
6475
6995
  })).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([])
6996
+ workflows: import_zod46.z.array(import_zod46.z.object({
6997
+ name: import_zod46.z.string(),
6998
+ condition: import_zod46.z.string().optional(),
6999
+ actions: import_zod46.z.array(import_zod46.z.record(import_zod46.z.unknown())).optional().default([])
6480
7000
  })).optional(),
6481
- calendars: import_zod45.z.array(import_zod45.z.object({
6482
- name: import_zod45.z.string(),
6483
- description: import_zod45.z.string().optional()
7001
+ calendars: import_zod46.z.array(import_zod46.z.object({
7002
+ name: import_zod46.z.string(),
7003
+ description: import_zod46.z.string().optional()
6484
7004
  })).optional()
6485
7005
  });
6486
7006
  function registerTemplateDeployerTools(server2, client) {
@@ -6551,7 +7071,7 @@ function registerTemplateDeployerTools(server2, client) {
6551
7071
  "get_template_questionnaire",
6552
7072
  "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
7073
  {
6554
- templateFile: import_zod45.z.string().describe("Path to the template JSON file (from list_templates).")
7074
+ templateFile: import_zod46.z.string().describe("Path to the template JSON file (from list_templates).")
6555
7075
  },
6556
7076
  async ({ templateFile }) => {
6557
7077
  try {
@@ -6584,10 +7104,10 @@ function registerTemplateDeployerTools(server2, client) {
6584
7104
  "deploy_template",
6585
7105
  "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
7106
  {
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.")
7107
+ templateFile: import_zod46.z.string().describe("Path to the template JSON file."),
7108
+ 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', ...})."),
7109
+ locationId: import_zod46.z.string().optional().describe("Location ID to deploy to. Uses default if not specified."),
7110
+ dryRun: import_zod46.z.boolean().optional().describe("If true, shows what would be created without actually creating anything. Defaults to false.")
6591
7111
  },
6592
7112
  async ({ templateFile, answers, locationId: locationId2, dryRun }) => {
6593
7113
  try {
@@ -6833,7 +7353,7 @@ ${errors.join("\n")}` : "\nNo errors!",
6833
7353
  }
6834
7354
 
6835
7355
  // src/tools/validators.ts
6836
- var import_zod46 = require("zod");
7356
+ var import_zod47 = require("zod");
6837
7357
  function extractFromTrigger(trigger, refs) {
6838
7358
  const triggerName = String(trigger.name ?? trigger.type ?? "unnamed trigger");
6839
7359
  const where = `trigger "${triggerName}"`;
@@ -6968,7 +7488,7 @@ function registerValidatorTools(server2, client, builderClient) {
6968
7488
  "validate_workflow",
6969
7489
  "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
7490
  {
6971
- workflowId: import_zod46.z.string().describe("The workflow ID to validate.")
7491
+ workflowId: import_zod47.z.string().describe("The workflow ID to validate.")
6972
7492
  },
6973
7493
  async ({ workflowId }) => {
6974
7494
  try {
@@ -7239,20 +7759,24 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
7239
7759
  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
7760
  }
7241
7761
  const result = await builderClient.checkAuth();
7762
+ const activeCompany = builderClient.getCurrentCompanyId();
7763
+ const companyNote = activeCompany ? ` Active company: ${activeCompany}.` : "";
7242
7764
  if (result.ok) {
7243
- return { name: "Firebase auth (workflow builder)", status: "pass", detail: "ID token refresh succeeded. Workflow builder tools are usable." };
7765
+ return { name: "Firebase auth (workflow builder)", status: "pass", detail: `ID token refresh succeeded. Workflow builder tools are usable.${companyNote}` };
7244
7766
  }
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.` };
7767
+ 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
7768
  })();
7247
7769
  const registryCheck = (() => {
7248
7770
  if (!registry2) {
7249
7771
  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
7772
  }
7251
7773
  const locs = registry2.listLocations();
7774
+ const companies = registry2.listCompanyFirebases();
7775
+ const companyNote = companies.length ? ` ${companies.length} company Firebase credential(s) registered for multi-tenant workflow-builder access.` : "";
7252
7776
  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." };
7777
+ return { name: "Token registry", status: "warn", detail: `Initialized but empty \u2014 register sub-accounts via register_location for cross-account switching.${companyNote}` };
7254
7778
  }
7255
- return { name: "Token registry", status: "pass", detail: `${locs.length} sub-account(s) registered.` };
7779
+ return { name: "Token registry", status: "pass", detail: `${locs.length} sub-account(s) registered.${companyNote}` };
7256
7780
  })();
7257
7781
  const [versionStatus, apiKeyCheck, locationCheck, firebaseCheck] = await Promise.all([
7258
7782
  versionPromise,
@@ -7345,220 +7869,6 @@ function registerAllTools(server2, client, registry2, mcpVersion) {
7345
7869
  registerLocationSwitcherTools(server2, client, builderClient, registry2, mcpVersion);
7346
7870
  }
7347
7871
 
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
7872
  // src/tools/meta.ts
7563
7873
  function registerMetaTools(server2, installedVersion) {
7564
7874
  server2.tool(