@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 +20 -0
- package/README.md +2 -1
- package/dist/index.js +162 -35
- package/package.json +1 -1
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 (
|
|
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.
|
|
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
|
-
|
|
600
|
-
|
|
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
|
-
|
|
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
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
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
|
-
|
|
6576
|
-
}
|
|
6577
|
-
const
|
|
6578
|
-
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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
|
|
8388
|
-
const
|
|
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
|
-
|
|
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.
|
|
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",
|