@elitedcs/ghl-mcp 3.20.0 → 3.23.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,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.23.0 — `update_calendar` can assign team members again (Henry fix, part 2)
4
+
5
+ **Critical fix. Tool count unchanged (212 across 43 modules). The `teamMembers` parameter is back on `update_calendar` — and actually persists this time — with a typed schema that surfaces GHL's strict validation up front.**
6
+
7
+ Reported by Henry Boulton, 2026-05-26.
8
+
9
+ **Root cause.** Same UI-vs-API pattern we hit in v3.22.0 (forms) and v3.15.0 (email templates). v3.21.0's diagnosis ("GHL silently drops `teamMembers` on PUT, no public-API path exists") was based on testing against an Event calendar — that calendar TYPE doesn't have team members, so GHL accepts the call and ignores the field. On round-robin and class-booking calendars the field is meaningful, and PUT validates it strictly: `priority` must be exactly `0`, `0.5`, or `1` (anything else 422s), `locationConfigurations` needs at least one entry, and `userId` / `selected` / `isPrimary` must be present. Henry's original attempt likely either targeted the wrong calendar type or passed a non-enum priority.
10
+
11
+ **Fix.** `teamMembers` is back on `update_calendar`, typed via an exported `CalendarTeamMemberSchema` that mirrors GHL's validation:
12
+
13
+ - `priority` constrained to `z.union([z.literal(0), z.literal(0.5), z.literal(1)])` — invalid values fail at the MCP layer with a clear error instead of an opaque GHL 422.
14
+ - `locationConfigurations` requires `min(1)` and a non-negative integer `position`.
15
+ - Tool description spells out that team-member assignment only applies to calendar types that support it (round_robin, class_booking); on Event calendars the field is silently ignored by GHL — that's the type, not us.
16
+
17
+ End-to-end verification 2026-05-26 against MCP Testing: create round_robin → assign user → update via PUT (priority 0.5 → 1, location string change) → GET back the new shape → cleanup.
18
+
19
+ Five new tests in `src/tools/tool-payloads.test.ts` lock the schema in.
20
+
21
+ ## 3.22.0 — `update_form` works again (Henry fix)
22
+
23
+ **Critical fix. Tool count unchanged (212 across 43 modules). The `update_form` tool, which v3.21.0 marked "currently unavailable," is back — wired to the live save endpoint and verified end-to-end against MCP Testing.**
24
+
25
+ Reported by Henry Boulton, 2026-05-26.
26
+
27
+ **Root cause.** v3.21.0's diagnosis was half right and half wrong. The half that was right: the v2 form builder UI does run in a cross-origin iframe (`leadgen-apps-form-survey-builder.leadconnectorhq.com`) and the UI does read form state through Firestore — top-frame network capture only sees a Firestore `Listen` channel, never a REST save. That's exactly what made the route look gone. The half that was wrong: we concluded the REST save route therefore *didn't exist* and the tool needed a Firestore-client integration. It does exist. The new UI just doesn't drive it. Same UI-vs-API divergence we saw with email templates in v3.15.0 ([reference_email_template_firestore](https://github.com/drjerryrelth/ghl-command-mcp/blob/main/CHANGELOG.md#3150)) — the REST routes survive even when the editor moves to Firestore.
28
+
29
+ **Fix.** `update_form` now calls `POST /forms/{formId}?locationId={loc}` with body exactly `{name, formData}`. Verified shape constraints (server rejects everything else with 422 "property X should not exist"):
30
+
31
+ - `locationId` belongs in the query string only — putting it in the body fails.
32
+ - `name` and `formData` are both required together — one without the other 422s.
33
+ - `_id`, `deleted`, `productType`, `dateAdded`, `dateUpdated`, `source`, `updatedBy`, `version`, `updatedAt`, `versionHistory` must not appear in the body — `get_form_full` → pass through fails, so callers should pass only the `formData` they want to write.
34
+
35
+ If `name` is omitted, the tool pre-fetches the current form (one extra GET) so the existing name is preserved. Body and path are factored into pure `buildUpdateFormBody` / `buildUpdateFormPath` helpers and locked in by `src/tools/tool-payloads.test.ts` (3 new tests) — a refactor can't silently regress the tool back to v3.21.0's "unavailable" state.
36
+
37
+ End-to-end verification 2026-05-26: read form (version 5) → mutate first field's `placeholder` → save (201) → re-read (placeholder persisted) → restore. MCP Testing sandbox left clean.
38
+
39
+ ## 3.21.0 — Workflow Builder now loads for npm-install buyers (Henry + Ryan fix)
40
+
41
+ **Critical fix. Tool count unchanged (212 across 43 modules), but for `npx`-install buyers the 49 Firebase-gated tools (workflow builder, funnel builder, form builder, pipeline builder, smart lists, reputation, email campaigns, memberships, email-builder internal, plus the pre-deploy validator) now actually register after `enable_workflow_builder` — previously they stayed "Not configured" no matter how many times you restarted.**
42
+
43
+ Reported independently by Henry Boulton and Ryan Thomas, 2026-05-26.
44
+
45
+ **Root cause.** Index startup folded `GHL_USER_ID` + the Firebase trio from `credentials.json` into `process.env`, but never folded `GHL_API_KEY` or `GHL_LOCATION_ID`. `WorkflowBuilderClient.fromEnv()` reads all five directly from `process.env`, so for the recommended `npx -y @elitedcs/ghl-mcp@latest` install path — where no env vars are set anywhere — `fromEnv()` returned `null` and every Firebase-gated tool was skipped. Wrapper-script installs (e.g. `start-mcp.sh`) were unaffected because they exported all the vars manually. Henry's workaround of passing the values as `--env` flags on `claude mcp add` "fixed" it by route, which is why this looked like a credentials-file persistence bug.
46
+
47
+ **Fix.** All credentials-file values now fold into `process.env` at startup (env still wins when already set, so wrapper-script + headless paths are unchanged). Pulled the fold into a single `foldCredentialsIntoEnv()` helper with regression tests that lock in all five Firebase-gated fields.
48
+
49
+ Also in this release:
50
+
51
+ - **`create_form`** now uses the correct path. GHL silently retired `POST /forms?locationId=…` (returns 404); the working path is `POST /forms/?locationId=…` (trailing slash). Verified against MCP Testing.
52
+ - **`update_form`** is honestly marked unavailable, with the actual root cause documented for the first time: the GHL form builder runs in a cross-origin iframe (`leadgen-apps-form-survey-builder.leadconnectorhq.com`) and writes saves **directly to Firestore via the Firebase JS SDK** — `firestore.googleapis.com/.../databases/(default)` against the `highlevel-backend` project — rather than any REST endpoint. That's why every REST probe returns 404 or "Form does not exist or is deleted." Wiring this up needs a Firestore-client integration (separate from the workflow-builder's REST-over-Firebase-token pattern). Tracked for a focused future release. Workaround: `delete_form` + `create_form` (both work).
53
+ - **`update_calendar`** removed `teamMembers` from the input. GHL's public API silently drops the field on PUT (200 OK, nothing persists) across every shape and API version we tested. The tool description now tells the caller to set the assignee in the GHL UI instead of returning a fake success.
54
+
3
55
  ## 3.20.0 — License gate for headless / env-var installs
4
56
 
5
57
  **Security + headless support. No tool changes (212 across 43 modules).**
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.20.0",
34
+ version: "3.23.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",
@@ -332,6 +332,23 @@ function readCredentials() {
332
332
  return null;
333
333
  }
334
334
  }
335
+ function foldCredentialsIntoEnv(creds, env = process.env) {
336
+ if (!creds) return;
337
+ const mappings = [
338
+ ["ghl_api_key", "GHL_API_KEY"],
339
+ ["ghl_location_id", "GHL_LOCATION_ID"],
340
+ ["ghl_user_id", "GHL_USER_ID"],
341
+ ["ghl_firebase_api_key", "GHL_FIREBASE_API_KEY"],
342
+ ["ghl_firebase_refresh_token", "GHL_FIREBASE_REFRESH_TOKEN"],
343
+ ["ghl_company_id", "GHL_COMPANY_ID"]
344
+ ];
345
+ for (const [credKey, envKey] of mappings) {
346
+ const value = creds[credKey];
347
+ if (value && !env[envKey]) {
348
+ env[envKey] = value;
349
+ }
350
+ }
351
+ }
335
352
  function writeCredentials(creds) {
336
353
  ensureAppDataDir();
337
354
  const file = credentialsPath();
@@ -2370,6 +2387,18 @@ function registerOpportunityTools(server2, client) {
2370
2387
 
2371
2388
  // src/tools/calendars.ts
2372
2389
  var import_zod9 = require("zod");
2390
+ var TeamMemberLocationConfigSchema = import_zod9.z.object({
2391
+ kind: import_zod9.z.string().describe("Meeting location kind. Common values: 'custom', 'zoom_conference', 'google_meet'."),
2392
+ position: import_zod9.z.number().int().nonnegative().describe("Position index for ordering (0-based)."),
2393
+ location: import_zod9.z.string().optional().describe("Display string for the meeting location (URL or address). Optional for kind='custom'.")
2394
+ });
2395
+ var CalendarTeamMemberSchema = import_zod9.z.object({
2396
+ userId: import_zod9.z.string().describe("GHL user ID to assign. Must exist in this sub-account."),
2397
+ priority: import_zod9.z.union([import_zod9.z.literal(0), import_zod9.z.literal(0.5), import_zod9.z.literal(1)]).describe("Booking priority. Exactly 0, 0.5, or 1 (anything else 422s from GHL). Higher = preferred for round-robin."),
2398
+ isPrimary: import_zod9.z.boolean().describe("Whether this user is the primary assignee."),
2399
+ selected: import_zod9.z.boolean().describe("Whether this user is currently active on the calendar (true to enable)."),
2400
+ locationConfigurations: import_zod9.z.array(TeamMemberLocationConfigSchema).min(1).describe("At least one meeting-location config. Typical shape: [{kind:'custom', position:0, location:''}].")
2401
+ });
2373
2402
  function registerCalendarTools(server2, client) {
2374
2403
  safeTool(
2375
2404
  server2,
@@ -2439,7 +2468,7 @@ function registerCalendarTools(server2, client) {
2439
2468
  safeTool(
2440
2469
  server2,
2441
2470
  "update_calendar",
2442
- "Update an existing calendar",
2471
+ "Update an existing calendar's settings. Supports name, description, slug, event title/color, slot durations, capacity, AND team-member assignment via `teamMembers`. Team members only apply to calendar types that use them (round_robin, class_booking, etc.) \u2014 on event calendars GHL accepts the call but silently ignores the field. priority MUST be 0, 0.5, or 1.",
2443
2472
  {
2444
2473
  calendarId: import_zod9.z.string().describe("The calendar ID to update"),
2445
2474
  name: import_zod9.z.string().optional().describe("Calendar name"),
@@ -2447,7 +2476,7 @@ function registerCalendarTools(server2, client) {
2447
2476
  slug: import_zod9.z.string().optional().describe("Calendar slug"),
2448
2477
  widgetSlug: import_zod9.z.string().optional().describe("Widget slug for embedding"),
2449
2478
  calendarType: import_zod9.z.string().optional().describe("Calendar type"),
2450
- teamMembers: import_zod9.z.array(import_zod9.z.record(import_zod9.z.unknown())).optional().describe("Array of team member objects"),
2479
+ teamMembers: import_zod9.z.array(CalendarTeamMemberSchema).optional().describe("Replace the calendar's team-member assignments. Pass [] to clear (round-robin calendars require at least one). Use get_calendar to inspect the current shape first."),
2451
2480
  eventTitle: import_zod9.z.string().optional().describe("Default event title"),
2452
2481
  eventColor: import_zod9.z.string().optional().describe("Event color hex code"),
2453
2482
  slotDuration: import_zod9.z.number().optional().describe("Slot duration in minutes"),
@@ -5482,6 +5511,12 @@ ${text2}`);
5482
5511
 
5483
5512
  // src/tools/form-builder.ts
5484
5513
  var import_zod35 = require("zod");
5514
+ function buildUpdateFormPath(formId, locationId2) {
5515
+ return `/${formId}?locationId=${locationId2}`;
5516
+ }
5517
+ function buildUpdateFormBody(name, formData) {
5518
+ return { name, formData };
5519
+ }
5485
5520
  function registerFormBuilderTools(server2, builderClient) {
5486
5521
  const client = builderClient;
5487
5522
  if (!client) return;
@@ -5525,17 +5560,28 @@ ${text2}`);
5525
5560
  );
5526
5561
  server2.tool(
5527
5562
  "update_form",
5528
- "Update a form's structure \u2014 fields, labels, conditional logic, auto-responder, email notifications, styling. Use get_form_full first to see the current structure, modify the formData object, and pass it here.",
5563
+ "Update a form's structure \u2014 fields, labels, conditional logic, auto-responder, email notifications, styling. Use get_form_full first to read the current form, modify the formData object, then pass it here. If name is omitted, the current name is preserved (one extra GET).",
5529
5564
  {
5530
5565
  formId: import_zod35.z.string().describe("The form ID to update."),
5531
5566
  formData: import_zod35.z.record(import_zod35.z.unknown()).describe("The updated formData object containing form fields, settings, autoResponder config, etc."),
5532
- name: import_zod35.z.string().optional().describe("Updated form name.")
5567
+ name: import_zod35.z.string().optional().describe("Updated form name. If omitted, the current name is preserved.")
5533
5568
  },
5534
5569
  async ({ formId, formData, name }) => {
5535
5570
  try {
5536
- const body = { formData, locationId: client.locationId };
5537
- if (name !== void 0) body.name = name;
5538
- const result = await formRequest("PUT", `/${formId}?locationId=${client.locationId}`, body);
5571
+ let resolvedName = name;
5572
+ if (resolvedName === void 0) {
5573
+ const current = await formRequest("GET", `/${formId}?locationId=${client.locationId}`);
5574
+ const currentName = typeof current === "object" && current !== null && "form" in current ? current.form?.name : void 0;
5575
+ if (typeof currentName !== "string") {
5576
+ throw new Error(`Could not resolve current form name for ${formId} (GET /forms/${formId} returned no string name).`);
5577
+ }
5578
+ resolvedName = currentName;
5579
+ }
5580
+ const result = await formRequest(
5581
+ "POST",
5582
+ buildUpdateFormPath(formId, client.locationId),
5583
+ buildUpdateFormBody(resolvedName, formData)
5584
+ );
5539
5585
  return {
5540
5586
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
5541
5587
  };
@@ -5552,7 +5598,7 @@ ${text2}`);
5552
5598
  },
5553
5599
  async ({ name }) => {
5554
5600
  try {
5555
- const result = await formRequest("POST", `?locationId=${client.locationId}`, {
5601
+ const result = await formRequest("POST", `/?locationId=${client.locationId}`, {
5556
5602
  name,
5557
5603
  locationId: client.locationId,
5558
5604
  formData: {
@@ -8303,18 +8349,7 @@ var registry = new TokenRegistry();
8303
8349
  var fileCreds = readCredentials();
8304
8350
  var locationId = process.env.GHL_LOCATION_ID || fileCreds?.ghl_location_id || void 0;
8305
8351
  var apiKey = process.env.GHL_API_KEY || fileCreds?.ghl_api_key || void 0;
8306
- if (fileCreds?.ghl_user_id && !process.env.GHL_USER_ID) {
8307
- process.env.GHL_USER_ID = fileCreds.ghl_user_id;
8308
- }
8309
- if (fileCreds?.ghl_firebase_api_key && !process.env.GHL_FIREBASE_API_KEY) {
8310
- process.env.GHL_FIREBASE_API_KEY = fileCreds.ghl_firebase_api_key;
8311
- }
8312
- if (fileCreds?.ghl_firebase_refresh_token && !process.env.GHL_FIREBASE_REFRESH_TOKEN) {
8313
- process.env.GHL_FIREBASE_REFRESH_TOKEN = fileCreds.ghl_firebase_refresh_token;
8314
- }
8315
- if (fileCreds?.ghl_company_id && !process.env.GHL_COMPANY_ID) {
8316
- process.env.GHL_COMPANY_ID = fileCreds.ghl_company_id;
8317
- }
8352
+ foldCredentialsIntoEnv(fileCreds);
8318
8353
  if (locationId && registry.hasTokens()) {
8319
8354
  const token = registry.getToken(locationId);
8320
8355
  if (token) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.20.0",
3
+ "version": "3.23.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",