@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 +52 -0
- package/dist/index.js +56 -21
- package/package.json +1 -1
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.
|
|
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(
|
|
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
|
|
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
|
-
|
|
5537
|
-
if (
|
|
5538
|
-
|
|
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",
|
|
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
|
-
|
|
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.
|
|
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",
|