@elitedcs/ghl-mcp 3.15.0 → 3.16.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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.16.0 — Membership/course creates, SMS templates, reviews list (Firebase-gated)
4
+
5
+ **212 tools across 43 modules (+6). Build courses and SMS templates from Claude; read the reviews that come back.**
6
+
7
+ Six new write/read tools, all reverse-engineered from DevTools captures of the live GHL UI against the MCP Testing sandbox and verified end-to-end (each returns 2xx with the exact request shape these tools send). All require Firebase auth — the public bearer key 401s on each — so the no-Firebase tool count stays 163.
8
+
9
+ ### New tools (6)
10
+
11
+ - **`create_course`** — create a membership course (a "product") with title + description.
12
+ - **`create_membership_category`** — add a category/module inside a course (groups lessons; supports drip days).
13
+ - **`create_membership_lesson`** — add a lesson (a "post") inside a category, with HTML body and content type.
14
+ - **`create_membership_offer`** — create the access grant that enrolls contacts into one or more courses. Free by default; supports recurring/one-time pricing.
15
+ - **`create_sms_template`** — create an SMS/text template (a "snippet") for the composer and SMS workflow actions; merge fields supported. Also creates email snippets via `type: "email"`.
16
+ - **`list_reviews`** — list the reviews a location has received (Google, Facebook, etc.) with rating, author, text, and reply status. **Fixes the long-standing "No Location Found" error:** the reviews endpoint resolves location through nested `filterParams[locationId][0][value]`, not a flat `locationId`. Responding to a review is still pending (needs a live review to capture).
17
+
18
+ Course/category/lesson/offer hit `backend.leadconnectorhq.com/membership/locations/{loc}/*`; reviews hit `backend.leadconnectorhq.com/reputation/reviews`; SMS templates hit `services.leadconnectorhq.com/snippets/{loc}`. All with the workflow-builder Firebase token.
19
+
20
+ ### Still pending
21
+
22
+ Email-campaign delete/send/schedule live in a cross-origin iframe (`email-home-prod.leadconnectorhq.com`) that can't be black-box captured and whose REST paths 404 on the public host — deferred until a capture from inside that iframe.
23
+
3
24
  ## 3.15.0 — Email-template delete, rename, archive (Firebase-gated)
4
25
 
5
26
  **206 tools across 43 modules (+3). Manage email templates without leaving Claude.**
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GHL Command — GoHighLevel MCP Server
2
2
 
3
- **Full GoHighLevel API access for Claude.** 206 tools across 43 modules — manage contacts, conversations, pipelines, calendars, funnels, workflows, invoices, custom objects, webhooks, and more. **Includes full workflow builder, funnel/page editor, form builder, pipeline builder, bulk operations, account export, and workflow cloning** — capabilities no other GHL tool offers. **Multi-tenant:** one install can run the workflow builder across multiple clients' GHL accounts.
3
+ **Full GoHighLevel API access for Claude.** 212 tools across 43 modules — manage contacts, conversations, pipelines, calendars, funnels, workflows, invoices, custom objects, webhooks, and more. **Includes full workflow builder, funnel/page editor, form builder, pipeline builder, bulk operations, account export, and workflow cloning** — capabilities no other GHL tool offers. **Multi-tenant:** one install can run the workflow builder across multiple clients' GHL accounts.
4
4
 
5
5
  **Distributed via npm as [`@elitedcs/ghl-mcp`](https://www.npmjs.com/package/@elitedcs/ghl-mcp).** Buyers install with one config block — no git, no Node.js setup, no terminal commands. Updates flow automatically (`npx @latest` re-resolves on every Claude restart).
6
6
 
package/dist/index.js CHANGED
@@ -31,8 +31,8 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@elitedcs/ghl-mcp",
34
- version: "3.15.0",
35
- description: "GoHighLevel MCP Server for Claude. 206 tools \u2014 full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
34
+ version: "3.16.0",
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: {
38
38
  "ghl-mcp": "dist/index.js"
@@ -3861,6 +3861,7 @@ function registerBlogTools(server2, client) {
3861
3861
  // src/tools/emails.ts
3862
3862
  var import_zod25 = require("zod");
3863
3863
  var EMAIL_BUILDER_BASE = "https://backend.leadconnectorhq.com/emails/builder";
3864
+ var SNIPPETS_BASE = "https://services.leadconnectorhq.com/snippets";
3864
3865
  var TEMPLATE_TYPES = ["html", "folder", "import", "builder", "blank", "ai_template", "vibe-editor"];
3865
3866
  var EDITOR_TYPES = ["html", "builder"];
3866
3867
  function registerEmailTools(server2, client) {
@@ -4024,6 +4025,44 @@ ${text2}`);
4024
4025
  }
4025
4026
  }
4026
4027
  );
4028
+ server2.tool(
4029
+ "create_sms_template",
4030
+ "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
+ {
4032
+ name: import_zod25.z.string().describe("Template/snippet name (shown in the snippets list, not sent to contacts)."),
4033
+ body: import_zod25.z.string().describe("The message body. SMS: plain text with optional merge fields. Email snippet: HTML."),
4034
+ type: import_zod25.z.enum(["sms", "email"]).optional().describe("Snippet type. Defaults to 'sms' (text). Use 'email' for an HTML email snippet."),
4035
+ locationId: import_zod25.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
4036
+ },
4037
+ async ({ name, body, type, locationId: locationId2 }) => {
4038
+ try {
4039
+ const loc = locationId2 ?? client.locationId;
4040
+ const headers = await client.buildHeaders();
4041
+ const response = await fetch(`${SNIPPETS_BASE}/${loc}`, {
4042
+ method: "POST",
4043
+ headers,
4044
+ body: JSON.stringify({
4045
+ name,
4046
+ template: { body, attachments: [] },
4047
+ useForLiveChat: false,
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}
4057
+ ${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
+ }
4064
+ }
4065
+ );
4027
4066
  }
4028
4067
 
4029
4068
  // src/tools/trigger-links.ts
@@ -5731,7 +5770,7 @@ async function validateFirebase(firebaseKey, refreshToken) {
5731
5770
  function registerSetupTool(server2) {
5732
5771
  server2.tool(
5733
5772
  "setup_ghl_mcp",
5734
- "First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 206 tools (163 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
5773
+ "First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 212 tools (163 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
5735
5774
  {
5736
5775
  email: import_zod37.z.string().email().describe("Email used at purchase."),
5737
5776
  license_key: import_zod37.z.string().min(20).describe("License key from your purchase email."),
@@ -5786,7 +5825,7 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
5786
5825
  ghl_firebase_api_key: workflowBuilderEnabled ? args.ghl_firebase_api_key?.trim() : void 0,
5787
5826
  ghl_firebase_refresh_token: workflowBuilderEnabled ? args.ghl_firebase_refresh_token?.trim() : void 0
5788
5827
  });
5789
- const toolCount = workflowBuilderEnabled ? "206" : "163";
5828
+ const toolCount = workflowBuilderEnabled ? "212" : "163";
5790
5829
  const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
5791
5830
  const wfTip = workflowBuilderEnabled ? "" : "\nTo enable Workflow Builder later (8 extra tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
5792
5831
  return {
@@ -6911,6 +6950,43 @@ ${text2}`);
6911
6950
  }
6912
6951
  }
6913
6952
  );
6953
+ server2.tool(
6954
+ "list_reviews",
6955
+ "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
+ {
6957
+ locationId: import_zod43.z.string().optional().describe("Location ID. Falls back to the active builder client's location."),
6958
+ pageNumber: import_zod43.z.number().optional().describe("1-based page number. Defaults to 1."),
6959
+ pageSize: import_zod43.z.number().optional().describe("Results per page. Defaults to 10."),
6960
+ rating: import_zod43.z.number().optional().describe("Optional: only return reviews with this star rating (1-5)."),
6961
+ includeDeleted: import_zod43.z.boolean().optional().describe("Include deleted reviews. Defaults to false.")
6962
+ },
6963
+ async ({ locationId: locationId2, pageNumber, pageSize, rating, includeDeleted }) => {
6964
+ try {
6965
+ const loc = locationId2 ?? client.locationId;
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
+ }
6988
+ }
6989
+ );
6914
6990
  }
6915
6991
 
6916
6992
  // src/tools/email-campaigns.ts
@@ -6974,12 +7050,16 @@ var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
6974
7050
  function registerMembershipTools(server2, builderClient) {
6975
7051
  const client = builderClient;
6976
7052
  if (!client) return;
6977
- async function membershipRequest(path6) {
7053
+ async function membershipRequest(path6, method = "GET", body) {
6978
7054
  const headers = await client.buildHeaders();
6979
- const response = await fetch(`${MEMBERSHIP_BASE}${path6}`, { method: "GET", headers });
7055
+ const response = await fetch(`${MEMBERSHIP_BASE}${path6}`, {
7056
+ method,
7057
+ headers,
7058
+ body: body ? JSON.stringify(body) : void 0
7059
+ });
6980
7060
  if (!response.ok) {
6981
7061
  const text2 = await response.text();
6982
- throw new Error(`Membership API Error ${response.status}: GET ${path6}
7062
+ throw new Error(`Membership API Error ${response.status}: ${method} ${path6}
6983
7063
  ${text2}`);
6984
7064
  }
6985
7065
  const text = await response.text();
@@ -7036,6 +7116,130 @@ ${text2}`);
7036
7116
  }
7037
7117
  }
7038
7118
  );
7119
+ server2.tool(
7120
+ "create_course",
7121
+ "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
+ {
7123
+ title: import_zod45.z.string().describe("Course title."),
7124
+ description: import_zod45.z.string().optional().describe("Course description. Defaults to empty."),
7125
+ locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
7126
+ },
7127
+ async ({ title, description, locationId: locationId2 }) => {
7128
+ try {
7129
+ const loc = locationId2 ?? client.locationId;
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
+ }
7138
+ }
7139
+ );
7140
+ server2.tool(
7141
+ "create_membership_category",
7142
+ "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
+ {
7144
+ title: import_zod45.z.string().describe("Category title (e.g. 'Module 1')."),
7145
+ productId: import_zod45.z.string().describe("The course/product id this category belongs to."),
7146
+ description: import_zod45.z.string().optional().describe("Category description. Defaults to empty."),
7147
+ visibility: import_zod45.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
7148
+ sequenceNo: import_zod45.z.number().optional().describe("Display order within the course. Defaults to 0."),
7149
+ dripDays: import_zod45.z.number().optional().describe("Days after enrollment before this category unlocks. Defaults to 0 (no drip)."),
7150
+ locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
7151
+ },
7152
+ async ({ title, productId, description, visibility, sequenceNo, dripDays, locationId: locationId2 }) => {
7153
+ try {
7154
+ const loc = locationId2 ?? client.locationId;
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
+ }
7173
+ }
7174
+ );
7175
+ server2.tool(
7176
+ "create_membership_lesson",
7177
+ "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
+ {
7179
+ title: import_zod45.z.string().describe("Lesson title."),
7180
+ categoryId: import_zod45.z.string().describe("The category id this lesson belongs to (from create_membership_category)."),
7181
+ productId: import_zod45.z.string().describe("The course/product id this lesson belongs to."),
7182
+ description: import_zod45.z.string().optional().describe("Lesson body as HTML. Defaults to empty."),
7183
+ contentType: import_zod45.z.enum(["video", "audio", "text", "pdf", "assignment"]).optional().describe("Lesson content type. Defaults to 'video'."),
7184
+ visibility: import_zod45.z.enum(["published", "draft"]).optional().describe("'published' (default) or 'draft'."),
7185
+ sequenceNo: import_zod45.z.number().optional().describe("Display order within the category. Defaults to 0."),
7186
+ locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
7187
+ },
7188
+ async ({ title, categoryId, productId, description, contentType, visibility, sequenceNo, locationId: locationId2 }) => {
7189
+ try {
7190
+ const loc = locationId2 ?? client.locationId;
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
+ }
7212
+ }
7213
+ );
7214
+ server2.tool(
7215
+ "create_membership_offer",
7216
+ "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
+ {
7218
+ title: import_zod45.z.string().describe("Offer title (shown at checkout / in the offer list)."),
7219
+ productIds: import_zod45.z.array(import_zod45.z.string()).describe("Course/product ids this offer grants access to (from create_course or list_membership_offers)."),
7220
+ type: import_zod45.z.enum(["free", "recurring", "one_time"]).optional().describe("Offer type. Defaults to 'free'."),
7221
+ amount: import_zod45.z.number().optional().describe("Price for paid offers. Defaults to 0 (free)."),
7222
+ currency: import_zod45.z.string().optional().describe("Currency code for paid offers. Defaults to 'USD'."),
7223
+ locationId: import_zod45.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
7224
+ },
7225
+ async ({ title, productIds, type, amount, currency, locationId: locationId2 }) => {
7226
+ try {
7227
+ const loc = locationId2 ?? client.locationId;
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
+ }
7241
+ }
7242
+ );
7039
7243
  }
7040
7244
 
7041
7245
  // src/tools/template-deployer.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.15.0",
4
- "description": "GoHighLevel MCP Server for Claude. 206 tools — full CRM, automation, marketing control, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
3
+ "version": "3.16.0",
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": {
7
7
  "ghl-mcp": "dist/index.js"