@elitedcs/ghl-mcp 3.26.0 → 3.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.27.0 — Multi-tenant Firebase binding fix
4
+
5
+ **Fixes the bug that blocked workflow-builder tools in client sub-accounts even after a successful `register_company_firebase`.**
6
+
7
+ **Root cause.** GHL uses two different company identifiers. The ID shown in the agency dashboard URL is NOT the `companyId` that `/locations/{id}` returns or that the Firebase token carries in its `company_id` claim. `register_company_firebase` stored the credentials under the human-typed (agency-URL) ID, but `switch_location` resolves a sub-account's owner from `/locations/{id}.companyId` — the internal ID. The two never matched, so the lookup missed, the workflow builder fell back to home auth, and every Firebase-gated call 401'd. The warning even told the user to run `register_company_firebase` again — which they already had.
8
+
9
+ **Fixes:**
10
+
11
+ - **Store under the token's real company.** `register_company_firebase` now decodes the Firebase ID token's `company_id` claim and keys the registry entry on THAT value, not the typed one. Pass any company ID you have; it self-corrects. A note tells you when the stored ID differs from what you entered.
12
+ - **Verification now exercises the real path.** The optional end-to-end test resolves the test location's company the same way `switch_location` does, confirms the registry lookup actually hits, then makes a live workflow-builder call. The old probe used an inline client that bypassed the lookup, so it passed even when the stored key would never be found.
13
+ - **PIT/Firebase mismatch detection.** Every time the client mints an ID token, it compares the token's `company_id` claim against the company we intend to act on. A mismatch (the exact signature of this bug, or any future regression) writes a loud one-time stderr warning instead of failing silently. Direct Firebase-gated tool calls bypass `switch_location`'s routing, so this lives at the token-mint layer.
14
+ - **Honest `health_check`.** Firebase auth now reports the company the token ACTUALLY authenticates as, and returns `fail` (not a bare `pass`) when that disagrees with the active company.
15
+ - **Registry key normalization.** Company IDs are trimmed on store/lookup so a stray pasted space can't fork one company into two entries.
16
+ - **Misleading warning fixed.** When a sub-account's company has no matching key, `switch_location` now distinguishes "nothing registered" from "registered under a different ID" and names the mismatch, instead of blindly telling the user to re-register.
17
+ - **Updated `register_company_firebase` description + DevTools doc** so we stop telling users to copy the company ID from the agency URL.
18
+
19
+ **Known behavior (by design):** after an MCP restart you must `switch_location` once into a client account before its workflow-builder tools work — the active company resets to home on launch. `health_check` and the mismatch warning now make this obvious.
20
+
21
+ **Tests:** new `workflow-builder-mismatch.test.ts` covers claim decoding, registry key normalization, mismatch warn/no-warn, and warn-once dedup. Full suite green (163 passing).
22
+
3
23
  ## 3.26.0 — Codex adversarial audit fixes
4
24
 
5
25
  **Tool count unchanged (213). No new features — closes a set of silent bugs Codex flagged in a third-party adversarial review of the v3.21.0–v3.25.0 release arc.**
package/README.md CHANGED
@@ -185,12 +185,13 @@ To unlock full builder access across multiple clients from one install:
185
185
  3. **Register the client's company Firebase:**
186
186
  ```
187
187
  register_company_firebase
188
- companyId: CLIENT_COMPANY_ID (shown in register_location's output)
188
+ companyId: CLIENT_COMPANY_ID (best-effort see note)
189
189
  name: "Client Name"
190
190
  ghl_firebase_refresh_token: FROM_STEP_7
191
191
  ghl_user_id: FROM_STEP_6
192
192
  test_location_id: CLIENT_LOCATION_ID (optional — runs a live check that it works)
193
193
  ```
194
+ > **`companyId` is best-effort.** GHL's agency-URL company ID is a *different* identifier than the internal one used for sub-accounts. The tool decodes the company ID the Firebase token itself authenticates as and stores the entry under that, so you don't have to track down the exact internal ID — pass whatever you have (e.g. from `register_location`'s output) and it self-corrects.
194
195
 
195
196
  4. **Switch and build:** `switch_location CLIENT_LOCATION_ID` swaps both the API key *and* the Firebase auth automatically. The workflow builder now operates in the client's account. Switching back to your own location restores your home Firebase.
196
197
 
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@elitedcs/ghl-mcp",
34
- version: "3.26.0",
34
+ version: "3.27.0",
35
35
  mcpName: "io.github.drjerryrelth/ghl-command",
36
36
  description: "GoHighLevel MCP Server for Claude. 212 tools \u2014 full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
37
37
  main: "dist/index.js",
@@ -404,7 +404,7 @@ var TokenRegistryDataSchema = import_zod2.z.object({
404
404
  // process operate the workflow builder across multiple clients' GHL accounts.
405
405
  firebaseByCompany: import_zod2.z.record(CompanyFirebaseSchema).optional()
406
406
  });
407
- var TokenRegistry = class {
407
+ var TokenRegistry = class _TokenRegistry {
408
408
  data;
409
409
  filePath;
410
410
  constructor(filePath) {
@@ -578,26 +578,36 @@ var TokenRegistry = class {
578
578
  this.save();
579
579
  }
580
580
  }
581
+ /**
582
+ * Normalize a company id used as a firebaseByCompany key. Trims whitespace so
583
+ * a stray copy/paste space can't fork one company into two registry entries
584
+ * (one stored, a different one looked up). Keys are otherwise case-sensitive
585
+ * because GHL company ids are case-sensitive Mongo-style ids.
586
+ */
587
+ static normalizeCompanyId(companyId) {
588
+ return companyId.trim();
589
+ }
581
590
  /**
582
591
  * Get a specific company's Firebase config (multi-tenant routing).
583
592
  */
584
593
  getCompanyFirebase(companyId) {
585
- return this.data.firebaseByCompany?.[companyId];
594
+ return this.data.firebaseByCompany?.[_TokenRegistry.normalizeCompanyId(companyId)];
586
595
  }
587
596
  /**
588
597
  * Store (or replace) a company's Firebase config.
589
598
  */
590
599
  setCompanyFirebase(companyId, config3) {
591
600
  if (!this.data.firebaseByCompany) this.data.firebaseByCompany = {};
592
- this.data.firebaseByCompany[companyId] = config3;
601
+ this.data.firebaseByCompany[_TokenRegistry.normalizeCompanyId(companyId)] = config3;
593
602
  this.save();
594
603
  }
595
604
  /**
596
605
  * Remove a company's Firebase config. Returns true if one existed.
597
606
  */
598
607
  removeCompanyFirebase(companyId) {
599
- if (this.data.firebaseByCompany?.[companyId]) {
600
- delete this.data.firebaseByCompany[companyId];
608
+ const key = _TokenRegistry.normalizeCompanyId(companyId);
609
+ if (this.data.firebaseByCompany?.[key]) {
610
+ delete this.data.firebaseByCompany[key];
601
611
  this.save();
602
612
  return true;
603
613
  }
@@ -619,7 +629,7 @@ var TokenRegistry = class {
619
629
  * company is the active one). No-op if the company isn't registered.
620
630
  */
621
631
  updateCompanyFirebaseRefreshToken(companyId, newToken) {
622
- const cfg = this.data.firebaseByCompany?.[companyId];
632
+ const cfg = this.data.firebaseByCompany?.[_TokenRegistry.normalizeCompanyId(companyId)];
623
633
  if (cfg) {
624
634
  cfg.refreshToken = newToken;
625
635
  this.save();
@@ -639,6 +649,24 @@ var path3 = __toESM(require("path"));
639
649
  var dotenv = __toESM(require("dotenv"));
640
650
  var import_zod4 = require("zod");
641
651
 
652
+ // src/firebase-claims.ts
653
+ function decodeFirebaseClaims(idToken) {
654
+ try {
655
+ const seg = idToken.split(".")[1];
656
+ if (!seg) return {};
657
+ const json = Buffer.from(seg.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8");
658
+ const claims = JSON.parse(json);
659
+ return {
660
+ companyId: typeof claims.company_id === "string" ? claims.company_id : void 0,
661
+ userId: typeof claims.user_id === "string" ? claims.user_id : void 0,
662
+ type: typeof claims.type === "string" ? claims.type : void 0,
663
+ role: typeof claims.role === "string" ? claims.role : void 0
664
+ };
665
+ } catch {
666
+ return {};
667
+ }
668
+ }
669
+
642
670
  // src/trigger-schemas.ts
643
671
  var import_zod3 = require("zod");
644
672
  var TriggerActionSchema = import_zod3.z.object({
@@ -1173,6 +1201,20 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
1173
1201
  cachedIdToken = null;
1174
1202
  tokenExpiry = 0;
1175
1203
  tokenRefreshPromise = null;
1204
+ // The company_id claim from the most recently minted ID token. Lets
1205
+ // health_check report the company we're ACTUALLY authenticated as (vs. the
1206
+ // one we intended), and backs PIT/Firebase mismatch detection.
1207
+ tokenCompanyId;
1208
+ // The company that OWNS the location we're operating on, as resolved by
1209
+ // switch_location from /locations/{id}.companyId. This is the company the
1210
+ // Firebase token MUST authenticate as. It is distinct from currentCompanyId
1211
+ // (the Firebase auth company): when routing falls back to home auth, the two
1212
+ // diverge and that IS the binding bug. undefined for home-only installs or
1213
+ // before any switch_location call.
1214
+ intendedCompanyId;
1215
+ // Mismatch warnings already emitted (keyed `intended->token`). Append-only so
1216
+ // we warn once per distinct mismatch for the life of the process.
1217
+ mismatchWarnedFor = /* @__PURE__ */ new Set();
1176
1218
  constructor(config3) {
1177
1219
  this.firebaseApiKey = config3.firebaseApiKey;
1178
1220
  this.refreshToken = config3.refreshToken;
@@ -1214,6 +1256,27 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
1214
1256
  getCurrentCompanyId() {
1215
1257
  return this.currentCompanyId;
1216
1258
  }
1259
+ /**
1260
+ * The company_id claim from the most recently minted ID token — the company we
1261
+ * are ACTUALLY authenticated as. undefined until the first token mint. When this
1262
+ * differs from getIntendedCompanyId() the binding is wrong (see performTokenRefresh).
1263
+ */
1264
+ getTokenCompanyId() {
1265
+ return this.tokenCompanyId;
1266
+ }
1267
+ /**
1268
+ * Record the company that owns the location we're operating on (resolved by
1269
+ * switch_location). The Firebase token MUST authenticate as this company; a
1270
+ * divergence is the binding bug. Set on every switch regardless of whether
1271
+ * Firebase routing found a matching credential, so health_check and the
1272
+ * mismatch detector can flag a silent fall-back to home auth.
1273
+ */
1274
+ setIntendedCompanyId(companyId) {
1275
+ this.intendedCompanyId = companyId;
1276
+ }
1277
+ getIntendedCompanyId() {
1278
+ return this.intendedCompanyId;
1279
+ }
1217
1280
  /**
1218
1281
  * Swap in a specific company's Firebase auth (multi-tenant). Used by
1219
1282
  * switch_location so the workflow builder authenticates against the company
@@ -1347,6 +1410,18 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
1347
1410
  const data = FirebaseTokenSchema.parse(await response.json());
1348
1411
  this.cachedIdToken = data.id_token;
1349
1412
  this.tokenExpiry = Date.now() + 55 * 60 * 1e3;
1413
+ this.tokenCompanyId = decodeFirebaseClaims(data.id_token).companyId;
1414
+ const expectedCompanyId = this.intendedCompanyId ?? this.currentCompanyId;
1415
+ if (expectedCompanyId && this.tokenCompanyId && expectedCompanyId !== this.tokenCompanyId) {
1416
+ const warnKey = `${expectedCompanyId}->${this.tokenCompanyId}`;
1417
+ if (!this.mismatchWarnedFor.has(warnKey)) {
1418
+ this.mismatchWarnedFor.add(warnKey);
1419
+ process.stderr.write(
1420
+ `[ghl-mcp] WARNING: Firebase/company mismatch. Operating on company ${expectedCompanyId} but the Firebase token authenticates as ${this.tokenCompanyId}. Workflow-builder writes will 401 or hit the wrong account. The company Firebase is missing or registered under the wrong ID \u2014 run register_company_firebase with a token captured from that sub-account's session.
1421
+ `
1422
+ );
1423
+ }
1424
+ }
1350
1425
  if (data.refresh_token && data.refresh_token !== this.refreshToken) {
1351
1426
  this.refreshToken = data.refresh_token;
1352
1427
  this.persistRotatedToken(data.refresh_token);
@@ -5999,7 +6074,9 @@ async function validateFirebase(firebaseKey, refreshToken) {
5999
6074
  signal: AbortSignal.timeout(1e4)
6000
6075
  });
6001
6076
  if (!res.ok) return { ok: false, error: "Firebase credentials rejected. Re-extract them from your GHL browser tab." };
6002
- return { ok: true };
6077
+ const data = await res.json().catch(() => ({}));
6078
+ const claims = data.id_token ? decodeFirebaseClaims(data.id_token) : {};
6079
+ return { ok: true, companyId: claims.companyId, userId: claims.userId };
6003
6080
  } catch (err) {
6004
6081
  const msg = err instanceof Error ? err.message : String(err);
6005
6082
  return { ok: false, error: `Could not reach Firebase: ${msg}` };
@@ -6306,12 +6383,18 @@ Firebase: using ${cfg.name || `company ${companyId}`} credentials \u2014 workflo
6306
6383
  }
6307
6384
  builderClient.resetToHomeFirebase();
6308
6385
  const homeCompanyId = builderClient.getCurrentCompanyId();
6309
- if (homeCompanyId === void 0) {
6310
- 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.";
6311
- }
6312
6386
  if (homeCompanyId === companyId) {
6313
6387
  return "\nFirebase: home company \u2014 workflow builder enabled.";
6314
6388
  }
6389
+ const registeredCompanies = registry2?.listCompanyFirebases?.() ?? [];
6390
+ if (registeredCompanies.length > 0) {
6391
+ const keys = registeredCompanies.map((c) => c.companyId).join(", ");
6392
+ return `
6393
+ Firebase: this sub-account's company is ${companyId}, but your registered company Firebase key(s) are [${keys}] \u2014 none match, so workflow-builder tools will 401 here. This is usually a company-ID mismatch: GHL's agency-URL ID differs from the internal company_id used for sub-accounts. Re-run register_company_firebase with a token captured from THIS sub-account's session (v3.27.0+ auto-stores it under the correct internal ID), or check list_registered_locations.`;
6394
+ }
6395
+ if (homeCompanyId === void 0) {
6396
+ 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.";
6397
+ }
6315
6398
  return `
6316
6399
  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.`;
6317
6400
  }
@@ -6393,6 +6476,9 @@ Token registry: ${registeredCount} location(s) registered${versionLine}`
6393
6476
  if (targetCompanyId && registry2?.getToken(locationId2)) {
6394
6477
  registry2.setLocationCompanyId(locationId2, targetCompanyId);
6395
6478
  }
6479
+ if (builderClient) {
6480
+ builderClient.setIntendedCompanyId(targetCompanyId);
6481
+ }
6396
6482
  const firebaseStatus = routeFirebaseForCompany(builderClient, registry2, targetCompanyId);
6397
6483
  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" : ""}`;
6398
6484
  return {
@@ -6516,9 +6602,9 @@ The API key could not access location ${locationId2}. Make sure:
6516
6602
  );
6517
6603
  server2.tool(
6518
6604
  "register_company_firebase",
6519
- "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.",
6605
+ "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. The tool stores them under the company ID the Firebase token itself authenticates as (decoded from the token), so you do NOT need to hunt down the exact internal company ID \u2014 pass whatever ID you have and it self-corrects. After this, switch_location to any of that company's locations authenticates the workflow builder correctly. DevTools capture steps: elitedcs.com/ghl-mcp-firebase.",
6520
6606
  {
6521
- 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."),
6607
+ companyId: import_zod38.z.string().describe("A GHL company/agency ID for this client (from switch_location/register_location output, or the GHL agency URL). Best-effort only: the tool re-keys the entry to the company ID the Firebase token actually authenticates as, which can differ from the ID shown in the agency URL. Just pass what you have."),
6522
6608
  name: import_zod38.z.string().describe("Friendly name for this client/company (e.g. 'Nathan \u2014 Acme Health')."),
6523
6609
  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."),
6524
6610
  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."),
@@ -6541,6 +6627,7 @@ The API key could not access location ${locationId2}. Make sure:
6541
6627
  }
6542
6628
  const refreshToken = args.ghl_firebase_refresh_token.trim();
6543
6629
  const userId = args.ghl_user_id.trim();
6630
+ const typedCompanyId = args.companyId.trim();
6544
6631
  const fb = await validateFirebase(apiKey2, refreshToken);
6545
6632
  if (!fb.ok) {
6546
6633
  return {
@@ -6550,7 +6637,14 @@ Capture fresh values from a browser session logged into THIS company's GHL. Step
6550
6637
  isError: true
6551
6638
  };
6552
6639
  }
6553
- registry2.setCompanyFirebase(args.companyId, { apiKey: apiKey2, refreshToken, userId, name: args.name });
6640
+ const canonicalCompanyId = fb.companyId || typedCompanyId;
6641
+ let keyNote = "";
6642
+ if (fb.companyId && fb.companyId !== typedCompanyId) {
6643
+ keyNote = `
6644
+
6645
+ Note: stored under the company ID the Firebase token actually uses (${fb.companyId}), not the value you entered (${typedCompanyId}). The ID in the GHL agency URL differs from the one GHL uses internally for sub-accounts; switch_location matches on the internal one, so this is what makes it work.`;
6646
+ }
6647
+ registry2.setCompanyFirebase(canonicalCompanyId, { apiKey: apiKey2, refreshToken, userId, name: args.name });
6554
6648
  let testLine = "";
6555
6649
  if (args.test_location_id) {
6556
6650
  const token = registry2.getToken(args.test_location_id);
@@ -6559,33 +6653,57 @@ Capture fresh values from a browser session logged into THIS company's GHL. Step
6559
6653
 
6560
6654
  Skipped end-to-end test: location ${args.test_location_id} isn't registered. Run register_location for it, then switch_location to confirm.`;
6561
6655
  } else {
6562
- const probe = new WorkflowBuilderClient({
6563
- firebaseApiKey: apiKey2,
6564
- refreshToken,
6565
- apiKey: token.apiKey,
6566
- locationId: args.test_location_id,
6567
- userId,
6568
- companyId: args.companyId,
6569
- registry: null
6570
- });
6571
- try {
6572
- await probe.listWorkflows(1, 0);
6656
+ let resolvedCompanyId = token.companyId;
6657
+ let lookupFailed = false;
6658
+ if (!resolvedCompanyId) {
6659
+ try {
6660
+ const locClient = new GHLClient({ apiKey: token.apiKey, locationId: args.test_location_id });
6661
+ const locResp = await locClient.get(`/locations/${args.test_location_id}`);
6662
+ resolvedCompanyId = locResp?.location?.companyId ?? locResp?.companyId;
6663
+ } catch {
6664
+ lookupFailed = true;
6665
+ }
6666
+ }
6667
+ if (!resolvedCompanyId) {
6573
6668
  testLine = `
6574
6669
 
6575
- Verified: the workflow-builder API responded for "${token.name}" (${args.test_location_id}). These credentials work for this company.`;
6576
- } catch (error) {
6577
- const message = error instanceof Error ? error.message : String(error);
6578
- testLine = `
6670
+ Registered under ${canonicalCompanyId}, but couldn't confirm the test location's company${lookupFailed ? " (its PIT may be missing or invalid \u2014 re-run register_location for it)" : ""}. switch_location still resolves the company live at switch time, so this is non-blocking \u2014 switch into the account to confirm.`;
6671
+ } else {
6672
+ const foundViaRealPath = registry2.getCompanyFirebase(resolvedCompanyId);
6673
+ if (!foundViaRealPath) {
6674
+ testLine = `
6675
+
6676
+ WARNING: saved under ${canonicalCompanyId}, but switch_location resolves ${args.test_location_id} to company ${resolvedCompanyId ?? "(unknown)"} \u2014 these do not match, so the workflow builder would fall back to home and 401. Re-capture the Firebase token from a session inside THIS sub-account's company, or pass a test_location_id that belongs to the same company as the token.`;
6677
+ } else {
6678
+ const probe = new WorkflowBuilderClient({
6679
+ firebaseApiKey: foundViaRealPath.apiKey,
6680
+ refreshToken: foundViaRealPath.refreshToken,
6681
+ apiKey: token.apiKey,
6682
+ locationId: args.test_location_id,
6683
+ userId: foundViaRealPath.userId,
6684
+ companyId: resolvedCompanyId,
6685
+ registry: null
6686
+ });
6687
+ try {
6688
+ await probe.listWorkflows(1, 0);
6689
+ testLine = `
6579
6690
 
6580
- WARNING: saved, but the end-to-end test FAILED for ${args.test_location_id}:
6691
+ Verified end-to-end via the real lookup path: switch_location resolves ${args.test_location_id} \u2192 company ${resolvedCompanyId}, the registry has matching Firebase, and the workflow-builder API responded for "${token.name}". This will work.`;
6692
+ } catch (error) {
6693
+ const message = error instanceof Error ? error.message : String(error);
6694
+ testLine = `
6695
+
6696
+ WARNING: key resolution matched but the workflow-builder API call FAILED for ${args.test_location_id}:
6581
6697
  ${message}
6582
6698
 
6583
- 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.`;
6699
+ Likely cause: that location's PIT key (registered via register_location) belongs to a different company than the Firebase token, or the token rotated. Re-capture from a session inside THIS company's GHL.`;
6700
+ }
6701
+ }
6584
6702
  }
6585
6703
  }
6586
6704
  }
6587
6705
  return {
6588
- content: [{ type: "text", text: `Registered Firebase for ${args.name} (company ${args.companyId}).${testLine}
6706
+ content: [{ type: "text", text: `Registered Firebase for ${args.name} (company ${canonicalCompanyId}).${keyNote}${testLine}
6589
6707
 
6590
6708
  Now run switch_location to any of this company's sub-accounts \u2014 the workflow builder will authenticate against it automatically.` }]
6591
6709
  };
@@ -8384,10 +8502,19 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
8384
8502
  return { name: "Firebase auth (workflow builder)", status: "skip", detail: "Not configured. The 49 Firebase-gated tools need Firebase credentials. The other 163 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). Do NOT put Firebase values as env vars in your Claude Desktop config \u2014 that path is unreliable and is the usual reason this still shows skip after a restart. enable_workflow_builder saves and verifies them for you." };
8385
8503
  }
8386
8504
  const result = await builderClient.checkAuth();
8387
- const activeCompany = builderClient.getCurrentCompanyId();
8388
- const companyNote = activeCompany ? ` Active company: ${activeCompany}.` : "";
8505
+ const tokenCompany = builderClient.getTokenCompanyId();
8506
+ const intendedCompany = builderClient.getIntendedCompanyId() ?? builderClient.getCurrentCompanyId();
8507
+ const companyNote = intendedCompany ? ` Active location's company: ${intendedCompany}.` : "";
8508
+ if (result.ok && intendedCompany && tokenCompany && intendedCompany !== tokenCompany) {
8509
+ return {
8510
+ name: "Firebase auth (workflow builder)",
8511
+ status: "fail",
8512
+ detail: `Company mismatch: the active location belongs to company ${intendedCompany}, but the Firebase token authenticates as ${tokenCompany}. Workflow-builder writes will 401 or hit the wrong account. This company's Firebase is missing or registered under the wrong ID. Run register_company_firebase with a token captured from that sub-account's GHL session (v3.27.0+ stores it under the correct internal ID automatically).`
8513
+ };
8514
+ }
8389
8515
  if (result.ok) {
8390
- return { name: "Firebase auth (workflow builder)", status: "pass", detail: `ID token refresh succeeded. Workflow builder tools are usable.${companyNote}` };
8516
+ const idNote = tokenCompany ? ` Authenticated as company ${tokenCompany}.` : "";
8517
+ return { name: "Firebase auth (workflow builder)", status: "pass", detail: `ID token refresh succeeded. Workflow builder tools are usable.${companyNote}${idNote}` };
8391
8518
  }
8392
8519
  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.` };
8393
8520
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.26.0",
3
+ "version": "3.27.0",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
5
  "description": "GoHighLevel MCP Server for Claude. 212 tools — full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",