@elitedcs/ghl-mcp 3.16.1 → 3.17.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 +15 -0
- package/README.md +1 -1
- package/dist/index.js +223 -215
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.17.0 — Multi-tenant bootstrap, internal hardening, test coverage
|
|
4
|
+
|
|
5
|
+
**No new tools — still 212 across 43 modules. Internal hardening + a multi-tenant fix.**
|
|
6
|
+
|
|
7
|
+
### Multi-tenant: bootstrap from a client company's Firebase
|
|
8
|
+
|
|
9
|
+
A multi-tenant-only install (an agency operating purely in clients' accounts, no home Firebase) previously got **none** of the Firebase-gated tools: `WorkflowBuilderClient.fromEnv()` returned null, so every gated module skipped registration even after `register_company_firebase`. Now, when there's no home Firebase, the builder client bootstraps from the first registered company that has both Firebase creds and a registered location (`fromFirstCompany`). `switch_location` still routes to other companies afterward. Token rotation on a bootstrapped install now persists back to that company's `firebaseByCompany` slot instead of being dropped (the same class of bug Don Harris hit with home-token persistence).
|
|
10
|
+
|
|
11
|
+
### Internal hardening
|
|
12
|
+
|
|
13
|
+
- **`safeTool()` consistency.** Migrated the Firebase-gated modules (memberships, reputation, email campaigns, email-template/snippet internal tools) from hand-rolled `try/catch` + `jsonResponse` to the `safeTool()` wrapper, matching the public-API tools and the type-safety standard.
|
|
14
|
+
- **Extracted request-body builders** (`buildCoursePayload`, `buildCategoryPayload`, `buildLessonPayload`, `buildOfferPayload`, `buildSnippetPayload`, `buildReviewsQuery`) as pure, exported functions.
|
|
15
|
+
- **+16 unit tests** (101 → 117) locking in the v3.16.0 request shapes and the nested-`filterParams` reviews query, plus the new multi-tenant bootstrap + its rotation persistence.
|
|
16
|
+
- **Doc count drift:** fixed remaining stale "8 / 30 / 168" tool counts in `setup_ghl_mcp`, `health_check`, and the README tool table (now consistently 163 base / 49 Firebase-gated / 212 total).
|
|
17
|
+
|
|
3
18
|
## 3.16.1 — Docs: correct enable_workflow_builder tool counts
|
|
4
19
|
|
|
5
20
|
**No tool changes. Still 212 tools across 43 modules (163 without Firebase, 49 Firebase-gated).**
|
package/README.md
CHANGED
|
@@ -451,7 +451,7 @@ To unlock full builder access across multiple clients from one install:
|
|
|
451
451
|
|---|---|
|
|
452
452
|
| `get_mcp_version` | Check installed version against the latest published to npm. Confirms an upgrade landed after restarting Claude. Available even before GHL credentials are configured. |
|
|
453
453
|
| `health_check` | Run a full health check: npm registry + version status, GHL API key validity, default location reachability, Firebase auth status, token registry presence. Use when something feels broken. |
|
|
454
|
-
| `enable_workflow_builder` | Add Firebase credentials to an existing install to unlock
|
|
454
|
+
| `enable_workflow_builder` | Add Firebase credentials to an existing install to unlock 49 additional Firebase-gated tools (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, memberships, validate_workflow). No need to re-enter license / API key / location ID. Run this any time after the basic setup, on the buyer's schedule. |
|
|
455
455
|
|
|
456
456
|
### Other Modules
|
|
457
457
|
|
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.17.0",
|
|
35
35
|
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.",
|
|
36
36
|
main: "dist/index.js",
|
|
37
37
|
bin: {
|
|
@@ -514,6 +514,19 @@ var TokenRegistry = class {
|
|
|
514
514
|
name: token.name
|
|
515
515
|
}));
|
|
516
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Find the first registered location belonging to a company. Used to
|
|
519
|
+
* bootstrap the workflow-builder client from a company Firebase entry when
|
|
520
|
+
* the install has no home Firebase (multi-tenant-only installs).
|
|
521
|
+
*/
|
|
522
|
+
firstLocationForCompany(companyId) {
|
|
523
|
+
for (const [locationId2, token] of Object.entries(this.data.tokens)) {
|
|
524
|
+
if (token.companyId === companyId) {
|
|
525
|
+
return { locationId: locationId2, apiKey: token.apiKey };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return void 0;
|
|
529
|
+
}
|
|
517
530
|
/**
|
|
518
531
|
* Update the home Firebase refresh token (called on rotation)
|
|
519
532
|
*/
|
|
@@ -1226,6 +1239,37 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
1226
1239
|
registry: registry2
|
|
1227
1240
|
});
|
|
1228
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Bootstrap from a registered CLIENT company's Firebase when there is no home
|
|
1244
|
+
* Firebase (multi-tenant-only install — e.g. an agency operating purely in
|
|
1245
|
+
* clients' accounts). Without this, fromEnv() returns null and every
|
|
1246
|
+
* Firebase-gated tool fails to register even after register_company_firebase.
|
|
1247
|
+
*
|
|
1248
|
+
* Picks the first company that has both Firebase creds and a registered
|
|
1249
|
+
* location, and makes that company the active (and "home") slot. switch_location
|
|
1250
|
+
* can still route to other companies afterward. Returns null if no such pair
|
|
1251
|
+
* exists. The chosen company is treated as home, so token rotation persists
|
|
1252
|
+
* back to its firebaseByCompany slot (see persistRotatedToken).
|
|
1253
|
+
*/
|
|
1254
|
+
static fromFirstCompany(registry2) {
|
|
1255
|
+
if (!registry2) return null;
|
|
1256
|
+
for (const { companyId } of registry2.listCompanyFirebases()) {
|
|
1257
|
+
const fb = registry2.getCompanyFirebase(companyId);
|
|
1258
|
+
const loc = registry2.firstLocationForCompany(companyId);
|
|
1259
|
+
if (fb && loc) {
|
|
1260
|
+
return new _WorkflowBuilderClient({
|
|
1261
|
+
firebaseApiKey: fb.apiKey,
|
|
1262
|
+
refreshToken: fb.refreshToken,
|
|
1263
|
+
apiKey: loc.apiKey,
|
|
1264
|
+
locationId: loc.locationId,
|
|
1265
|
+
userId: fb.userId,
|
|
1266
|
+
companyId,
|
|
1267
|
+
registry: registry2
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1229
1273
|
/**
|
|
1230
1274
|
* Get a fresh Firebase ID token using the refresh token.
|
|
1231
1275
|
* Uses a promise lock to prevent concurrent refresh races.
|
|
@@ -1278,9 +1322,12 @@ var WorkflowBuilderClient = class _WorkflowBuilderClient {
|
|
|
1278
1322
|
*/
|
|
1279
1323
|
persistRotatedToken(newToken) {
|
|
1280
1324
|
const company = this.currentCompanyId;
|
|
1281
|
-
const
|
|
1282
|
-
if (
|
|
1325
|
+
const hasCompanySlot = company !== void 0 && this.registry?.getCompanyFirebase(company) !== void 0;
|
|
1326
|
+
if (hasCompanySlot) {
|
|
1283
1327
|
this.registry?.updateCompanyFirebaseRefreshToken(company, newToken);
|
|
1328
|
+
if (company === this.homeCompanyId) {
|
|
1329
|
+
this.homeRefreshToken = newToken;
|
|
1330
|
+
}
|
|
1284
1331
|
return;
|
|
1285
1332
|
}
|
|
1286
1333
|
if (company === void 0 || company === this.homeCompanyId) {
|
|
@@ -3972,7 +4019,8 @@ ${text2}`);
|
|
|
3972
4019
|
const text = await response.text();
|
|
3973
4020
|
return text ? JSON.parse(text) : { ok: true };
|
|
3974
4021
|
}
|
|
3975
|
-
|
|
4022
|
+
safeTool(
|
|
4023
|
+
server2,
|
|
3976
4024
|
"delete_email_template",
|
|
3977
4025
|
"Permanently delete an email template ('builder') by id. This is a HARD delete \u2014 the template is removed, not archived (use archive_email_template if you want a reversible remove). Requires Firebase auth; the public bearer API can't do this. Get the templateId from list_email_templates. WARNING: irreversible. If the template is referenced by a workflow email action or a draft campaign, deleting it will break that reference.",
|
|
3978
4026
|
{
|
|
@@ -3980,16 +4028,12 @@ ${text2}`);
|
|
|
3980
4028
|
locationId: import_zod25.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
3981
4029
|
},
|
|
3982
4030
|
async ({ templateId, locationId: locationId2 }) => {
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
const result = await builderRequest("DELETE", `/data/${templateId}?locationId=${loc}`);
|
|
3986
|
-
return jsonResponse(result);
|
|
3987
|
-
} catch (error) {
|
|
3988
|
-
return errorResponse(error);
|
|
3989
|
-
}
|
|
4031
|
+
const loc = locationId2 ?? client.locationId;
|
|
4032
|
+
return builderRequest("DELETE", `/data/${templateId}?locationId=${loc}`);
|
|
3990
4033
|
}
|
|
3991
4034
|
);
|
|
3992
|
-
|
|
4035
|
+
safeTool(
|
|
4036
|
+
server2,
|
|
3993
4037
|
"rename_email_template",
|
|
3994
4038
|
"Rename an existing email template ('builder'). Updates the display title only; the HTML content and sender settings are left untouched. Requires Firebase auth; the public bearer API can't do this. Get the templateId from list_email_templates.",
|
|
3995
4039
|
{
|
|
@@ -3998,16 +4042,12 @@ ${text2}`);
|
|
|
3998
4042
|
locationId: import_zod25.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
3999
4043
|
},
|
|
4000
4044
|
async ({ templateId, name, locationId: locationId2 }) => {
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
const result = await builderRequest("PATCH", `/${templateId}`, { locationId: loc, name });
|
|
4004
|
-
return jsonResponse(result);
|
|
4005
|
-
} catch (error) {
|
|
4006
|
-
return errorResponse(error);
|
|
4007
|
-
}
|
|
4045
|
+
const loc = locationId2 ?? client.locationId;
|
|
4046
|
+
return builderRequest("PATCH", `/${templateId}`, { locationId: loc, name });
|
|
4008
4047
|
}
|
|
4009
4048
|
);
|
|
4010
|
-
|
|
4049
|
+
safeTool(
|
|
4050
|
+
server2,
|
|
4011
4051
|
"archive_email_template",
|
|
4012
4052
|
"Archive (or unarchive) an email template ('builder'). Archiving removes it from the active templates list without deleting it \u2014 a reversible alternative to delete_email_template. Requires Firebase auth. Get the templateId from list_email_templates.",
|
|
4013
4053
|
{
|
|
@@ -4016,16 +4056,12 @@ ${text2}`);
|
|
|
4016
4056
|
locationId: import_zod25.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
4017
4057
|
},
|
|
4018
4058
|
async ({ templateId, archived, locationId: locationId2 }) => {
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
const result = await builderRequest("PATCH", `/${templateId}`, { locationId: loc, archived: archived ?? true });
|
|
4022
|
-
return jsonResponse(result);
|
|
4023
|
-
} catch (error) {
|
|
4024
|
-
return errorResponse(error);
|
|
4025
|
-
}
|
|
4059
|
+
const loc = locationId2 ?? client.locationId;
|
|
4060
|
+
return builderRequest("PATCH", `/${templateId}`, { locationId: loc, archived: archived ?? true });
|
|
4026
4061
|
}
|
|
4027
4062
|
);
|
|
4028
|
-
|
|
4063
|
+
safeTool(
|
|
4064
|
+
server2,
|
|
4029
4065
|
"create_sms_template",
|
|
4030
4066
|
"Create an SMS/text template (a 'snippet') for quick-insert into the conversations composer and SMS workflow actions. The body can include merge fields like {{contact.first_name}}. Set type to 'email' to create an HTML email snippet instead. Returns the created snippet including its id. Requires Firebase auth \u2014 the public bearer key returns 401 on this endpoint.",
|
|
4031
4067
|
{
|
|
@@ -4035,35 +4071,34 @@ ${text2}`);
|
|
|
4035
4071
|
locationId: import_zod25.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
4036
4072
|
},
|
|
4037
4073
|
async ({ name, body, type, locationId: locationId2 }) => {
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
urlAttachments: [],
|
|
4049
|
-
type: type ?? "sms",
|
|
4050
|
-
isFolder: false,
|
|
4051
|
-
parentId: ""
|
|
4052
|
-
})
|
|
4053
|
-
});
|
|
4054
|
-
if (!response.ok) {
|
|
4055
|
-
const text2 = await response.text();
|
|
4056
|
-
throw new Error(`Snippets API Error ${response.status}: POST /snippets/${loc}
|
|
4074
|
+
const loc = locationId2 ?? client.locationId;
|
|
4075
|
+
const headers = await client.buildHeaders();
|
|
4076
|
+
const response = await fetch(`${SNIPPETS_BASE}/${loc}`, {
|
|
4077
|
+
method: "POST",
|
|
4078
|
+
headers,
|
|
4079
|
+
body: JSON.stringify(buildSnippetPayload({ name, body, type }))
|
|
4080
|
+
});
|
|
4081
|
+
if (!response.ok) {
|
|
4082
|
+
const text2 = await response.text();
|
|
4083
|
+
throw new Error(`Snippets API Error ${response.status}: POST /snippets/${loc}
|
|
4057
4084
|
${text2}`);
|
|
4058
|
-
}
|
|
4059
|
-
const text = await response.text();
|
|
4060
|
-
return jsonResponse(text ? JSON.parse(text) : { ok: true });
|
|
4061
|
-
} catch (error) {
|
|
4062
|
-
return errorResponse(error);
|
|
4063
4085
|
}
|
|
4086
|
+
const text = await response.text();
|
|
4087
|
+
return text ? JSON.parse(text) : { ok: true };
|
|
4064
4088
|
}
|
|
4065
4089
|
);
|
|
4066
4090
|
}
|
|
4091
|
+
function buildSnippetPayload(o) {
|
|
4092
|
+
return {
|
|
4093
|
+
name: o.name,
|
|
4094
|
+
template: { body: o.body, attachments: [] },
|
|
4095
|
+
useForLiveChat: false,
|
|
4096
|
+
urlAttachments: [],
|
|
4097
|
+
type: o.type ?? "sms",
|
|
4098
|
+
isFolder: false,
|
|
4099
|
+
parentId: ""
|
|
4100
|
+
};
|
|
4101
|
+
}
|
|
4067
4102
|
|
|
4068
4103
|
// src/tools/trigger-links.ts
|
|
4069
4104
|
var import_zod26 = require("zod");
|
|
@@ -5827,7 +5862,7 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
|
|
|
5827
5862
|
});
|
|
5828
5863
|
const toolCount = workflowBuilderEnabled ? "212" : "163";
|
|
5829
5864
|
const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
|
|
5830
|
-
const wfTip = workflowBuilderEnabled ? "" : "\nTo enable Workflow Builder later (
|
|
5865
|
+
const wfTip = workflowBuilderEnabled ? "" : "\nTo enable Workflow Builder later (49 extra Firebase-gated tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
|
|
5831
5866
|
return {
|
|
5832
5867
|
content: [{
|
|
5833
5868
|
type: "text",
|
|
@@ -6934,23 +6969,20 @@ ${text2}`);
|
|
|
6934
6969
|
if (!text) return {};
|
|
6935
6970
|
return JSON.parse(text);
|
|
6936
6971
|
}
|
|
6937
|
-
|
|
6972
|
+
safeTool(
|
|
6973
|
+
server2,
|
|
6938
6974
|
"get_review_link_list",
|
|
6939
|
-
"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.
|
|
6975
|
+
"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.",
|
|
6940
6976
|
{
|
|
6941
6977
|
locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
6942
6978
|
},
|
|
6943
6979
|
async ({ locationId: locationId2 }) => {
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
const result = await reputationRequest("GET", `/integrations/review-link-list?locationId=${loc}`);
|
|
6947
|
-
return jsonResponse(result);
|
|
6948
|
-
} catch (error) {
|
|
6949
|
-
return errorResponse(error);
|
|
6950
|
-
}
|
|
6980
|
+
const loc = locationId2 ?? client.locationId;
|
|
6981
|
+
return reputationRequest("GET", `/integrations/review-link-list?locationId=${loc}`);
|
|
6951
6982
|
}
|
|
6952
6983
|
);
|
|
6953
|
-
|
|
6984
|
+
safeTool(
|
|
6985
|
+
server2,
|
|
6954
6986
|
"list_reviews",
|
|
6955
6987
|
"List the reviews a location has received (Google, Facebook, etc.) with rating, author, text, reply status, and source. Supports paging and an optional rating filter. NOTE: location is resolved through nested filter params internally \u2014 a flat locationId is what caused the long-standing 'No Location Found' error, now fixed. Responding to a review is not yet available via API. Requires Firebase auth.",
|
|
6956
6988
|
{
|
|
@@ -6961,33 +6993,31 @@ ${text2}`);
|
|
|
6961
6993
|
includeDeleted: import_zod43.z.boolean().optional().describe("Include deleted reviews. Defaults to false.")
|
|
6962
6994
|
},
|
|
6963
6995
|
async ({ locationId: locationId2, pageNumber, pageSize, rating, includeDeleted }) => {
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
const q = [
|
|
6967
|
-
`filterParams[locationId][0][value]=${encodeURIComponent(loc)}`,
|
|
6968
|
-
`filterParams[locationId][0][condition]=eq`,
|
|
6969
|
-
`filterParams[deleted][0][value]=${includeDeleted ? "true" : "false"}`,
|
|
6970
|
-
`filterParams[deleted][0][condition]=eq`
|
|
6971
|
-
];
|
|
6972
|
-
if (rating !== void 0) {
|
|
6973
|
-
q.push(`filterParams[rating][0][value]=${rating}`);
|
|
6974
|
-
q.push(`filterParams[rating][0][condition]=eq`);
|
|
6975
|
-
}
|
|
6976
|
-
q.push(`sortParams[dateAdded]=-1`);
|
|
6977
|
-
q.push(`pageNumber=${pageNumber ?? 1}`);
|
|
6978
|
-
q.push(`pageSize=${pageSize ?? 10}`);
|
|
6979
|
-
const query = q.map((p) => {
|
|
6980
|
-
const i = p.indexOf("=");
|
|
6981
|
-
return `${encodeURIComponent(p.slice(0, i))}=${p.slice(i + 1)}`;
|
|
6982
|
-
}).join("&");
|
|
6983
|
-
const result = await reputationRequest("GET", `/reviews?${query}`);
|
|
6984
|
-
return jsonResponse(result);
|
|
6985
|
-
} catch (error) {
|
|
6986
|
-
return errorResponse(error);
|
|
6987
|
-
}
|
|
6996
|
+
const loc = locationId2 ?? client.locationId;
|
|
6997
|
+
return reputationRequest("GET", buildReviewsQuery(loc, { pageNumber, pageSize, rating, includeDeleted }));
|
|
6988
6998
|
}
|
|
6989
6999
|
);
|
|
6990
7000
|
}
|
|
7001
|
+
function buildReviewsQuery(locationId2, opts = {}) {
|
|
7002
|
+
const q = [
|
|
7003
|
+
`filterParams[locationId][0][value]=${locationId2}`,
|
|
7004
|
+
`filterParams[locationId][0][condition]=eq`,
|
|
7005
|
+
`filterParams[deleted][0][value]=${opts.includeDeleted ? "true" : "false"}`,
|
|
7006
|
+
`filterParams[deleted][0][condition]=eq`
|
|
7007
|
+
];
|
|
7008
|
+
if (opts.rating !== void 0) {
|
|
7009
|
+
q.push(`filterParams[rating][0][value]=${opts.rating}`);
|
|
7010
|
+
q.push(`filterParams[rating][0][condition]=eq`);
|
|
7011
|
+
}
|
|
7012
|
+
q.push(`sortParams[dateAdded]=-1`);
|
|
7013
|
+
q.push(`pageNumber=${opts.pageNumber ?? 1}`);
|
|
7014
|
+
q.push(`pageSize=${opts.pageSize ?? 10}`);
|
|
7015
|
+
const query = q.map((p) => {
|
|
7016
|
+
const i = p.indexOf("=");
|
|
7017
|
+
return `${encodeURIComponent(p.slice(0, i))}=${encodeURIComponent(p.slice(i + 1))}`;
|
|
7018
|
+
}).join("&");
|
|
7019
|
+
return `/reviews?${query}`;
|
|
7020
|
+
}
|
|
6991
7021
|
|
|
6992
7022
|
// src/tools/email-campaigns.ts
|
|
6993
7023
|
var import_zod44 = require("zod");
|
|
@@ -6995,7 +7025,8 @@ var SVC_BASE = "https://services.leadconnectorhq.com";
|
|
|
6995
7025
|
function registerEmailCampaignTools(server2, builderClient) {
|
|
6996
7026
|
const client = builderClient;
|
|
6997
7027
|
if (!client) return;
|
|
6998
|
-
|
|
7028
|
+
safeTool(
|
|
7029
|
+
server2,
|
|
6999
7030
|
"create_email_campaign",
|
|
7000
7031
|
"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.",
|
|
7001
7032
|
{
|
|
@@ -7010,36 +7041,32 @@ function registerEmailCampaignTools(server2, builderClient) {
|
|
|
7010
7041
|
locationId: import_zod44.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
7011
7042
|
},
|
|
7012
7043
|
async ({ templateId, name, subject, fromName, fromEmail, isPlainText, enableResendToUnopened, hasUtmTracking, locationId: locationId2 }) => {
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
throw new Error(`Email Campaign API Error ${response.status}: POST /emails/schedule
|
|
7044
|
+
const loc = locationId2 ?? client.locationId;
|
|
7045
|
+
const body = { locationId: loc, templateId };
|
|
7046
|
+
if (name !== void 0) body.name = name;
|
|
7047
|
+
if (subject !== void 0) body.subject = subject;
|
|
7048
|
+
if (fromName !== void 0) body.fromName = fromName;
|
|
7049
|
+
if (fromEmail !== void 0) body.fromEmail = fromEmail;
|
|
7050
|
+
if (isPlainText !== void 0) body.isPlainText = isPlainText;
|
|
7051
|
+
if (enableResendToUnopened !== void 0) body.enableResendToUnopened = enableResendToUnopened;
|
|
7052
|
+
if (hasUtmTracking !== void 0) body.hasUtmTracking = hasUtmTracking;
|
|
7053
|
+
const headers = await client.buildHeaders();
|
|
7054
|
+
const response = await fetch(`${SVC_BASE}/emails/schedule`, {
|
|
7055
|
+
method: "POST",
|
|
7056
|
+
headers,
|
|
7057
|
+
body: JSON.stringify(body)
|
|
7058
|
+
});
|
|
7059
|
+
if (!response.ok) {
|
|
7060
|
+
const text2 = await response.text();
|
|
7061
|
+
throw new Error(`Email Campaign API Error ${response.status}: POST /emails/schedule
|
|
7032
7062
|
${text2}`);
|
|
7033
|
-
}
|
|
7034
|
-
const text = await response.text();
|
|
7035
|
-
const result = text ? JSON.parse(text) : {};
|
|
7036
|
-
return jsonResponse({
|
|
7037
|
-
...result,
|
|
7038
|
-
_note: "Campaign created as a DRAFT. To send or schedule it, open it in the GHL UI (Marketing \u2192 Emails) \u2014 the send endpoint isn't available via API. To delete a draft, use the GHL UI."
|
|
7039
|
-
});
|
|
7040
|
-
} catch (error) {
|
|
7041
|
-
return errorResponse(error);
|
|
7042
7063
|
}
|
|
7064
|
+
const text = await response.text();
|
|
7065
|
+
const result = text ? JSON.parse(text) : {};
|
|
7066
|
+
return {
|
|
7067
|
+
...result,
|
|
7068
|
+
_note: "Campaign created as a DRAFT. To send or schedule it, open it in the GHL UI (Marketing \u2192 Emails) \u2014 the send endpoint isn't available via API. To delete a draft, use the GHL UI."
|
|
7069
|
+
};
|
|
7043
7070
|
}
|
|
7044
7071
|
);
|
|
7045
7072
|
}
|
|
@@ -7066,23 +7093,20 @@ ${text2}`);
|
|
|
7066
7093
|
if (!text) return {};
|
|
7067
7094
|
return JSON.parse(text);
|
|
7068
7095
|
}
|
|
7069
|
-
|
|
7096
|
+
safeTool(
|
|
7097
|
+
server2,
|
|
7070
7098
|
"list_membership_offers",
|
|
7071
|
-
"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.
|
|
7099
|
+
"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.",
|
|
7072
7100
|
{
|
|
7073
7101
|
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
7074
7102
|
},
|
|
7075
7103
|
async ({ locationId: locationId2 }) => {
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
const result = await membershipRequest(`/smart-list/offers-products/${loc}`);
|
|
7079
|
-
return jsonResponse(result);
|
|
7080
|
-
} catch (error) {
|
|
7081
|
-
return errorResponse(error);
|
|
7082
|
-
}
|
|
7104
|
+
const loc = locationId2 ?? client.locationId;
|
|
7105
|
+
return membershipRequest(`/smart-list/offers-products/${loc}`);
|
|
7083
7106
|
}
|
|
7084
7107
|
);
|
|
7085
|
-
|
|
7108
|
+
safeTool(
|
|
7109
|
+
server2,
|
|
7086
7110
|
"list_membership_categories",
|
|
7087
7111
|
"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.",
|
|
7088
7112
|
{
|
|
@@ -7090,16 +7114,12 @@ ${text2}`);
|
|
|
7090
7114
|
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
7091
7115
|
},
|
|
7092
7116
|
async ({ limit, locationId: locationId2 }) => {
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
const result = await membershipRequest(`/smart-list/location/${loc}/workflow?type=category&limit=${limit ?? 1e5}`);
|
|
7096
|
-
return jsonResponse(result);
|
|
7097
|
-
} catch (error) {
|
|
7098
|
-
return errorResponse(error);
|
|
7099
|
-
}
|
|
7117
|
+
const loc = locationId2 ?? client.locationId;
|
|
7118
|
+
return membershipRequest(`/smart-list/location/${loc}/workflow?type=category&limit=${limit ?? 1e5}`);
|
|
7100
7119
|
}
|
|
7101
7120
|
);
|
|
7102
|
-
|
|
7121
|
+
safeTool(
|
|
7122
|
+
server2,
|
|
7103
7123
|
"list_membership_lessons",
|
|
7104
7124
|
"List all membership/course lessons in a location. Use the returned ids with the lesson_completed / lesson_started trigger conditions. READ-ONLY.",
|
|
7105
7125
|
{
|
|
@@ -7107,16 +7127,12 @@ ${text2}`);
|
|
|
7107
7127
|
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
7108
7128
|
},
|
|
7109
7129
|
async ({ limit, locationId: locationId2 }) => {
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
const result = await membershipRequest(`/smart-list/location/${loc}/workflow?type=lesson&limit=${limit ?? 1e5}`);
|
|
7113
|
-
return jsonResponse(result);
|
|
7114
|
-
} catch (error) {
|
|
7115
|
-
return errorResponse(error);
|
|
7116
|
-
}
|
|
7130
|
+
const loc = locationId2 ?? client.locationId;
|
|
7131
|
+
return membershipRequest(`/smart-list/location/${loc}/workflow?type=lesson&limit=${limit ?? 1e5}`);
|
|
7117
7132
|
}
|
|
7118
7133
|
);
|
|
7119
|
-
|
|
7134
|
+
safeTool(
|
|
7135
|
+
server2,
|
|
7120
7136
|
"create_course",
|
|
7121
7137
|
"Create a membership course (a 'product') in a location. Creates the course shell with a title and description; add categories (create_membership_category) and lessons (create_membership_lesson) into it, and an offer (create_membership_offer) to grant access. Returns the new product, including its id. Requires Firebase auth.",
|
|
7122
7138
|
{
|
|
@@ -7125,19 +7141,12 @@ ${text2}`);
|
|
|
7125
7141
|
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
7126
7142
|
},
|
|
7127
7143
|
async ({ title, description, locationId: locationId2 }) => {
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
const result = await membershipRequest(`/locations/${loc}/products`, "POST", {
|
|
7131
|
-
title,
|
|
7132
|
-
description: description ?? ""
|
|
7133
|
-
});
|
|
7134
|
-
return jsonResponse(result);
|
|
7135
|
-
} catch (error) {
|
|
7136
|
-
return errorResponse(error);
|
|
7137
|
-
}
|
|
7144
|
+
const loc = locationId2 ?? client.locationId;
|
|
7145
|
+
return membershipRequest(`/locations/${loc}/products`, "POST", buildCoursePayload({ title, description }));
|
|
7138
7146
|
}
|
|
7139
7147
|
);
|
|
7140
|
-
|
|
7148
|
+
safeTool(
|
|
7149
|
+
server2,
|
|
7141
7150
|
"create_membership_category",
|
|
7142
7151
|
"Create a category (module/section) inside a membership course. Categories group lessons. Needs the productId of the course (from create_course or list_membership_offers). Returns the new category, including its id (use it as categoryId when creating lessons). Requires Firebase auth.",
|
|
7143
7152
|
{
|
|
@@ -7150,29 +7159,12 @@ ${text2}`);
|
|
|
7150
7159
|
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
7151
7160
|
},
|
|
7152
7161
|
async ({ title, productId, description, visibility, sequenceNo, dripDays, locationId: locationId2 }) => {
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
const result = await membershipRequest(`/locations/${loc}/categories`, "POST", {
|
|
7156
|
-
title,
|
|
7157
|
-
productId,
|
|
7158
|
-
visibility: visibility ?? "published",
|
|
7159
|
-
sequenceNo: sequenceNo ?? 0,
|
|
7160
|
-
dripDays: dripDays ?? 0,
|
|
7161
|
-
description: description ?? "",
|
|
7162
|
-
parentCategory: null,
|
|
7163
|
-
posterImage: "",
|
|
7164
|
-
lockedBy: null,
|
|
7165
|
-
lockedByCategory: null,
|
|
7166
|
-
commentPermission: null,
|
|
7167
|
-
metadata: null
|
|
7168
|
-
});
|
|
7169
|
-
return jsonResponse(result);
|
|
7170
|
-
} catch (error) {
|
|
7171
|
-
return errorResponse(error);
|
|
7172
|
-
}
|
|
7162
|
+
const loc = locationId2 ?? client.locationId;
|
|
7163
|
+
return membershipRequest(`/locations/${loc}/categories`, "POST", buildCategoryPayload({ title, productId, description, visibility, sequenceNo, dripDays }));
|
|
7173
7164
|
}
|
|
7174
7165
|
);
|
|
7175
|
-
|
|
7166
|
+
safeTool(
|
|
7167
|
+
server2,
|
|
7176
7168
|
"create_membership_lesson",
|
|
7177
7169
|
"Create a lesson (a 'post') inside a membership course category. Needs both the categoryId (from create_membership_category) and the productId of the course. Description is the lesson body as HTML. Returns the new lesson, including its id. Requires Firebase auth.",
|
|
7178
7170
|
{
|
|
@@ -7186,32 +7178,12 @@ ${text2}`);
|
|
|
7186
7178
|
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
7187
7179
|
},
|
|
7188
7180
|
async ({ title, categoryId, productId, description, contentType, visibility, sequenceNo, locationId: locationId2 }) => {
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
const result = await membershipRequest(`/locations/${loc}/posts`, "POST", {
|
|
7192
|
-
title,
|
|
7193
|
-
description: description ?? "",
|
|
7194
|
-
categoryId,
|
|
7195
|
-
productId,
|
|
7196
|
-
visibility: visibility ?? "published",
|
|
7197
|
-
sequenceNo: sequenceNo ?? 0,
|
|
7198
|
-
posterImage: null,
|
|
7199
|
-
commentStatus: "visible",
|
|
7200
|
-
contentType: contentType ?? "video",
|
|
7201
|
-
commentPermission: "enabled",
|
|
7202
|
-
lockedByPost: null,
|
|
7203
|
-
lockedByCategory: null,
|
|
7204
|
-
certificateTemplateId: null,
|
|
7205
|
-
metaData: null,
|
|
7206
|
-
contentId: null
|
|
7207
|
-
});
|
|
7208
|
-
return jsonResponse(result);
|
|
7209
|
-
} catch (error) {
|
|
7210
|
-
return errorResponse(error);
|
|
7211
|
-
}
|
|
7181
|
+
const loc = locationId2 ?? client.locationId;
|
|
7182
|
+
return membershipRequest(`/locations/${loc}/posts`, "POST", buildLessonPayload({ title, categoryId, productId, description, contentType, visibility, sequenceNo }));
|
|
7212
7183
|
}
|
|
7213
7184
|
);
|
|
7214
|
-
|
|
7185
|
+
safeTool(
|
|
7186
|
+
server2,
|
|
7215
7187
|
"create_membership_offer",
|
|
7216
7188
|
"Create a membership offer \u2014 the access grant that enrolls contacts into one or more courses/products. Link it to course product ids. Defaults to a free offer; for paid, set type to 'recurring' or 'one_time' with an amount. Returns the new offer, including its id (referenced by the offer_access_granted trigger). Requires Firebase auth.",
|
|
7217
7189
|
{
|
|
@@ -7223,24 +7195,60 @@ ${text2}`);
|
|
|
7223
7195
|
locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
7224
7196
|
},
|
|
7225
7197
|
async ({ title, productIds, type, amount, currency, locationId: locationId2 }) => {
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
const result = await membershipRequest(`/locations/${loc}/offers`, "POST", {
|
|
7229
|
-
title,
|
|
7230
|
-
type: type ?? "free",
|
|
7231
|
-
isLivePaymentMode: true,
|
|
7232
|
-
locationId: loc,
|
|
7233
|
-
productIds,
|
|
7234
|
-
amount: amount ?? 0,
|
|
7235
|
-
currency: currency ?? "USD"
|
|
7236
|
-
});
|
|
7237
|
-
return jsonResponse(result);
|
|
7238
|
-
} catch (error) {
|
|
7239
|
-
return errorResponse(error);
|
|
7240
|
-
}
|
|
7198
|
+
const loc = locationId2 ?? client.locationId;
|
|
7199
|
+
return membershipRequest(`/locations/${loc}/offers`, "POST", buildOfferPayload({ title, productIds, type, amount, currency, locationId: loc }));
|
|
7241
7200
|
}
|
|
7242
7201
|
);
|
|
7243
7202
|
}
|
|
7203
|
+
function buildCoursePayload(o) {
|
|
7204
|
+
return { title: o.title, description: o.description ?? "" };
|
|
7205
|
+
}
|
|
7206
|
+
function buildCategoryPayload(o) {
|
|
7207
|
+
return {
|
|
7208
|
+
title: o.title,
|
|
7209
|
+
productId: o.productId,
|
|
7210
|
+
visibility: o.visibility ?? "published",
|
|
7211
|
+
sequenceNo: o.sequenceNo ?? 0,
|
|
7212
|
+
dripDays: o.dripDays ?? 0,
|
|
7213
|
+
description: o.description ?? "",
|
|
7214
|
+
parentCategory: null,
|
|
7215
|
+
posterImage: "",
|
|
7216
|
+
lockedBy: null,
|
|
7217
|
+
lockedByCategory: null,
|
|
7218
|
+
commentPermission: null,
|
|
7219
|
+
metadata: null
|
|
7220
|
+
};
|
|
7221
|
+
}
|
|
7222
|
+
function buildLessonPayload(o) {
|
|
7223
|
+
return {
|
|
7224
|
+
title: o.title,
|
|
7225
|
+
description: o.description ?? "",
|
|
7226
|
+
categoryId: o.categoryId,
|
|
7227
|
+
productId: o.productId,
|
|
7228
|
+
visibility: o.visibility ?? "published",
|
|
7229
|
+
sequenceNo: o.sequenceNo ?? 0,
|
|
7230
|
+
posterImage: null,
|
|
7231
|
+
commentStatus: "visible",
|
|
7232
|
+
contentType: o.contentType ?? "video",
|
|
7233
|
+
commentPermission: "enabled",
|
|
7234
|
+
lockedByPost: null,
|
|
7235
|
+
lockedByCategory: null,
|
|
7236
|
+
certificateTemplateId: null,
|
|
7237
|
+
metaData: null,
|
|
7238
|
+
contentId: null
|
|
7239
|
+
};
|
|
7240
|
+
}
|
|
7241
|
+
function buildOfferPayload(o) {
|
|
7242
|
+
return {
|
|
7243
|
+
title: o.title,
|
|
7244
|
+
type: o.type ?? "free",
|
|
7245
|
+
isLivePaymentMode: true,
|
|
7246
|
+
locationId: o.locationId,
|
|
7247
|
+
productIds: o.productIds,
|
|
7248
|
+
amount: o.amount ?? 0,
|
|
7249
|
+
currency: o.currency ?? "USD"
|
|
7250
|
+
};
|
|
7251
|
+
}
|
|
7244
7252
|
|
|
7245
7253
|
// src/tools/template-deployer.ts
|
|
7246
7254
|
var import_zod46 = require("zod");
|
|
@@ -8033,7 +8041,7 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
|
|
|
8033
8041
|
})();
|
|
8034
8042
|
const firebasePromise = (async () => {
|
|
8035
8043
|
if (!builderClient) {
|
|
8036
|
-
return { name: "Firebase auth (workflow builder)", status: "skip", detail: "Not configured. The
|
|
8044
|
+
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)." };
|
|
8037
8045
|
}
|
|
8038
8046
|
const result = await builderClient.checkAuth();
|
|
8039
8047
|
const activeCompany = builderClient.getCurrentCompanyId();
|
|
@@ -8131,7 +8139,7 @@ function registerAllTools(server2, client, registry2, mcpVersion) {
|
|
|
8131
8139
|
for (const [register] of publicApiTools) {
|
|
8132
8140
|
register(server2, client);
|
|
8133
8141
|
}
|
|
8134
|
-
const builderClient = WorkflowBuilderClient.fromEnv(registry2);
|
|
8142
|
+
const builderClient = WorkflowBuilderClient.fromEnv(registry2) ?? WorkflowBuilderClient.fromFirstCompany(registry2);
|
|
8135
8143
|
registerWorkflowBuilderTools(server2, builderClient);
|
|
8136
8144
|
registerFunnelBuilderTools(server2, builderClient);
|
|
8137
8145
|
registerFormBuilderTools(server2, builderClient);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.0",
|
|
4
4
|
"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.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|