@elitedcs/ghl-mcp 3.34.9 → 3.35.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 +47 -0
- package/dist/index.js +128 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.35.0 — `create_social_post` is now draft-first (closes the silent "everything published at once" trap)
|
|
4
|
+
|
|
5
|
+
**Behavior change.** `create_social_post` no longer defers go-live to GoHighLevel's
|
|
6
|
+
server-side default. Before, when `status` was omitted the field was dropped from the
|
|
7
|
+
request and GHL decided whether the post went live — an undocumented default that, in
|
|
8
|
+
a loop, is exactly the "all posts fired at once, no error, no warning" failure operators
|
|
9
|
+
hit. The most expensive failures are the silent ones, so the tool now takes a defensive
|
|
10
|
+
stance by default.
|
|
11
|
+
|
|
12
|
+
- **Draft-first default.** Omit `status` and the post is created as a `draft` (editable,
|
|
13
|
+
never live). Going live is now an explicit choice (`status:"published"`).
|
|
14
|
+
- **Scheduling is coupled to status.** Pass `scheduledAt` with no `status` and it
|
|
15
|
+
auto-resolves to `"scheduled"`. Pass `scheduledAt` alongside any *other* status, or
|
|
16
|
+
`status:"scheduled"` with no `scheduledAt`, and the call is rejected with an actionable
|
|
17
|
+
error instead of silently publishing now or falling back to the server default. This
|
|
18
|
+
kills the "I thought it was scheduled" class of failure (CHANGELOG 3.18.0 always
|
|
19
|
+
required both fields; nothing enforced the pairing until now).
|
|
20
|
+
- Logic extracted to the pure, tested `resolvePostStatus(status, scheduledAt)` helper;
|
|
21
|
+
tool and field descriptions updated to document the contract. 5 regression tests added.
|
|
22
|
+
- **Migration:** any caller that relied on an un-statused post going live must now pass
|
|
23
|
+
`status:"published"` explicitly. No other tool is affected.
|
|
24
|
+
|
|
25
|
+
## 3.34.10 — `get_free_slots` accepts ISO dates; `update_form` name fallback for fresh forms
|
|
26
|
+
|
|
27
|
+
Two small fixes that close rough edges found while live-verifying v3.34.9 through
|
|
28
|
+
the MCP tools. Neither changes a proven capability; both have trivial workarounds.
|
|
29
|
+
|
|
30
|
+
- **`get_free_slots` now accepts ISO dates (its documented input) — not just epoch
|
|
31
|
+
milliseconds.** GHL's `GET /calendars/{id}/free-slots` requires `startDate` /
|
|
32
|
+
`endDate` as epoch **milliseconds** and 422s (`startDate must be a number…`) on an
|
|
33
|
+
ISO date, yet the tool's own schema documented `YYYY-MM-DD` and forwarded it
|
|
34
|
+
unconverted — so the documented input always failed. The tool now accepts a bare
|
|
35
|
+
`YYYY-MM-DD` (taken as start-of-day for `startDate`, end-of-day for `endDate`, in
|
|
36
|
+
the supplied `timezone`, default UTC), a full ISO datetime (`Date.parse`), or an
|
|
37
|
+
already-numeric epoch-millis value (passed through, back-compat) and converts to
|
|
38
|
+
the epoch ms GHL needs. Logic extracted to `buildFreeSlotsParams` / `toEpochMillis`
|
|
39
|
+
with tests (incl. the live Phoenix anchor `2026-06-15 → 1781506800000`).
|
|
40
|
+
- **`update_form` no longer throws on the natural `create_form` → `update_form`
|
|
41
|
+
sequence.** When `name` is omitted, `update_form` reads the current name to
|
|
42
|
+
preserve it. A form freshly created by `create_form` has no `name` in its builder
|
|
43
|
+
doc until its first save, so that read returned none and the call threw
|
|
44
|
+
(`Could not resolve current form name`). It now falls back to the public forms list
|
|
45
|
+
(which carries the name) and only errors — with actionable guidance to pass `name`
|
|
46
|
+
— if both sources come up empty. The form-builder tools now also receive the public
|
|
47
|
+
`GHLClient` for this lookup (it already had Firebase auth for the builder routes).
|
|
48
|
+
Helpers `pickFormName` / `findFormNameInList` extracted, with tests.
|
|
49
|
+
|
|
3
50
|
## 3.34.9 — Calendar availability: create_calendar / update_calendar now set `openHours`
|
|
4
51
|
|
|
5
52
|
Closes the on-camera "assign availability hours" gap. `create_calendar` and
|
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.35.0",
|
|
35
35
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
36
36
|
description: "GoHighLevel MCP Server for Claude. 218 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
37
37
|
main: "dist/index.js",
|
|
@@ -2779,6 +2779,61 @@ function buildUpdateCalendarBody(args) {
|
|
|
2779
2779
|
applyCalendarFields(body, args);
|
|
2780
2780
|
return body;
|
|
2781
2781
|
}
|
|
2782
|
+
function tzOffsetMillis(utcMillis, timeZone) {
|
|
2783
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
2784
|
+
timeZone,
|
|
2785
|
+
hourCycle: "h23",
|
|
2786
|
+
year: "numeric",
|
|
2787
|
+
month: "2-digit",
|
|
2788
|
+
day: "2-digit",
|
|
2789
|
+
hour: "2-digit",
|
|
2790
|
+
minute: "2-digit",
|
|
2791
|
+
second: "2-digit"
|
|
2792
|
+
});
|
|
2793
|
+
const parts = dtf.formatToParts(new Date(utcMillis));
|
|
2794
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value);
|
|
2795
|
+
const asUTC = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour"), get("minute"), get("second"));
|
|
2796
|
+
return asUTC - utcMillis;
|
|
2797
|
+
}
|
|
2798
|
+
function zonedStartOfDayMillis(year, month, day, timeZone) {
|
|
2799
|
+
const utcGuess = Date.UTC(year, month - 1, day, 0, 0, 0, 0);
|
|
2800
|
+
if (!timeZone) return utcGuess;
|
|
2801
|
+
return utcGuess - tzOffsetMillis(utcGuess, timeZone);
|
|
2802
|
+
}
|
|
2803
|
+
function zonedDayBoundaryMillis(year, month, day, endOfDay, timeZone) {
|
|
2804
|
+
if (endOfDay) return zonedStartOfDayMillis(year, month, day + 1, timeZone) - 1;
|
|
2805
|
+
return zonedStartOfDayMillis(year, month, day, timeZone);
|
|
2806
|
+
}
|
|
2807
|
+
function toEpochMillis(value, opts = {}) {
|
|
2808
|
+
const trimmed = value.trim();
|
|
2809
|
+
if (/^\d+$/.test(trimmed)) {
|
|
2810
|
+
if (trimmed.length === 13) return trimmed;
|
|
2811
|
+
throw new Error(`Ambiguous numeric date "${value}" for free-slots \u2014 pass epoch MILLISECONDS (13 digits), a YYYY-MM-DD date, or a full ISO datetime.`);
|
|
2812
|
+
}
|
|
2813
|
+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
|
|
2814
|
+
if (dateOnly) {
|
|
2815
|
+
const millis = zonedDayBoundaryMillis(
|
|
2816
|
+
Number(dateOnly[1]),
|
|
2817
|
+
Number(dateOnly[2]),
|
|
2818
|
+
Number(dateOnly[3]),
|
|
2819
|
+
Boolean(opts.endOfDay),
|
|
2820
|
+
opts.timeZone
|
|
2821
|
+
);
|
|
2822
|
+
return String(millis);
|
|
2823
|
+
}
|
|
2824
|
+
const parsed = Date.parse(trimmed);
|
|
2825
|
+
if (!Number.isNaN(parsed)) return String(parsed);
|
|
2826
|
+
throw new Error(`Invalid date "${value}" for free-slots \u2014 use YYYY-MM-DD, a full ISO datetime, or epoch milliseconds.`);
|
|
2827
|
+
}
|
|
2828
|
+
function buildFreeSlotsParams(args) {
|
|
2829
|
+
const params = {
|
|
2830
|
+
startDate: toEpochMillis(args.startDate, { timeZone: args.timezone }),
|
|
2831
|
+
endDate: toEpochMillis(args.endDate, { endOfDay: true, timeZone: args.timezone })
|
|
2832
|
+
};
|
|
2833
|
+
if (args.timezone !== void 0) params.timezone = args.timezone;
|
|
2834
|
+
if (args.userId !== void 0) params.userId = args.userId;
|
|
2835
|
+
return params;
|
|
2836
|
+
}
|
|
2782
2837
|
function registerCalendarTools(server2, client) {
|
|
2783
2838
|
safeTool(
|
|
2784
2839
|
server2,
|
|
@@ -2881,15 +2936,13 @@ function registerCalendarTools(server2, client) {
|
|
|
2881
2936
|
"Get available free slots for a calendar",
|
|
2882
2937
|
{
|
|
2883
2938
|
calendarId: import_zod9.z.string().describe("The calendar ID"),
|
|
2884
|
-
startDate: import_zod9.z.string().describe("Start
|
|
2885
|
-
endDate: import_zod9.z.string().describe("End
|
|
2939
|
+
startDate: import_zod9.z.string().describe("Start of the window. Accepts a date (YYYY-MM-DD), a full ISO datetime, or epoch milliseconds. A bare date is taken as start-of-day in `timezone` (UTC if unset). GHL's API needs epoch ms; the tool converts for you."),
|
|
2940
|
+
endDate: import_zod9.z.string().describe("End of the window. Accepts a date (YYYY-MM-DD), a full ISO datetime, or epoch milliseconds. A bare date is taken as end-of-day in `timezone` (UTC if unset)."),
|
|
2886
2941
|
timezone: import_zod9.z.string().optional().describe("Timezone (e.g. America/New_York)"),
|
|
2887
2942
|
userId: import_zod9.z.string().optional().describe("Filter by user ID")
|
|
2888
2943
|
},
|
|
2889
2944
|
async ({ calendarId, startDate, endDate, timezone, userId }) => {
|
|
2890
|
-
const params = { startDate, endDate };
|
|
2891
|
-
if (timezone !== void 0) params.timezone = timezone;
|
|
2892
|
-
if (userId !== void 0) params.userId = userId;
|
|
2945
|
+
const params = buildFreeSlotsParams({ startDate, endDate, timezone, userId });
|
|
2893
2946
|
return await client.get(`/calendars/${calendarId}/free-slots`, {
|
|
2894
2947
|
params
|
|
2895
2948
|
});
|
|
@@ -4001,6 +4054,21 @@ function registerMediaTools(server2, client) {
|
|
|
4001
4054
|
|
|
4002
4055
|
// src/tools/social-planner.ts
|
|
4003
4056
|
var import_zod21 = require("zod");
|
|
4057
|
+
function resolvePostStatus(status, scheduledAt) {
|
|
4058
|
+
if (scheduledAt !== void 0 && status !== void 0 && status !== "scheduled") {
|
|
4059
|
+
throw new Error(
|
|
4060
|
+
`create_social_post: scheduledAt was provided but status is "${status}". A scheduled post requires status:"scheduled" (or omit status to auto-schedule).`
|
|
4061
|
+
);
|
|
4062
|
+
}
|
|
4063
|
+
if (status === "scheduled" && scheduledAt === void 0) {
|
|
4064
|
+
throw new Error(
|
|
4065
|
+
'create_social_post: status:"scheduled" requires scheduledAt (an ISO publish time).'
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
4068
|
+
if (status !== void 0) return status;
|
|
4069
|
+
if (scheduledAt !== void 0) return "scheduled";
|
|
4070
|
+
return "draft";
|
|
4071
|
+
}
|
|
4004
4072
|
function registerSocialPlannerTools(server2, client) {
|
|
4005
4073
|
safeTool(
|
|
4006
4074
|
server2,
|
|
@@ -4072,7 +4140,7 @@ function registerSocialPlannerTools(server2, client) {
|
|
|
4072
4140
|
safeTool(
|
|
4073
4141
|
server2,
|
|
4074
4142
|
"create_social_post",
|
|
4075
|
-
|
|
4143
|
+
'Create a social media post. Defaults to a draft (never publishes live) unless you pass status explicitly. To schedule, pass scheduledAt and the status auto-resolves to "scheduled". To post immediately, pass status:"published".',
|
|
4076
4144
|
{
|
|
4077
4145
|
locationId: import_zod21.z.string().optional().describe("GHL Location ID (optional if GHL_LOCATION_ID is set)"),
|
|
4078
4146
|
accountIds: import_zod21.z.array(import_zod21.z.string()).describe("Array of social account IDs to post to"),
|
|
@@ -4080,8 +4148,12 @@ function registerSocialPlannerTools(server2, client) {
|
|
|
4080
4148
|
summary: import_zod21.z.string().optional().describe("The post text/caption"),
|
|
4081
4149
|
media: import_zod21.z.array(import_zod21.z.string()).optional().describe("Array of media URLs to attach"),
|
|
4082
4150
|
type: import_zod21.z.enum(["post", "story", "reel"]).optional().describe("Post type (default post)"),
|
|
4083
|
-
status: import_zod21.z.enum(["in_review", "scheduled", "draft", "published"]).optional().describe(
|
|
4084
|
-
|
|
4151
|
+
status: import_zod21.z.enum(["in_review", "scheduled", "draft", "published"]).optional().describe(
|
|
4152
|
+
'Post status. Omit to default to "draft" (safe \u2014 never goes live). Use "published" to post immediately, "scheduled" (with scheduledAt) to schedule, or "in_review" for an approval queue.'
|
|
4153
|
+
),
|
|
4154
|
+
scheduledAt: import_zod21.z.string().optional().describe(
|
|
4155
|
+
'Scheduled publish time (ISO string). When set, status auto-resolves to "scheduled" if you omit it; passing any other status alongside scheduledAt is rejected.'
|
|
4156
|
+
),
|
|
4085
4157
|
tags: import_zod21.z.array(import_zod21.z.string()).optional().describe("Tags for the post"),
|
|
4086
4158
|
ogData: import_zod21.z.object({
|
|
4087
4159
|
title: import_zod21.z.string().optional(),
|
|
@@ -4119,7 +4191,7 @@ function registerSocialPlannerTools(server2, client) {
|
|
|
4119
4191
|
};
|
|
4120
4192
|
body.media = media.map((url) => ({ url, type: mimeFor(url) }));
|
|
4121
4193
|
}
|
|
4122
|
-
|
|
4194
|
+
body.status = resolvePostStatus(status, scheduledAt);
|
|
4123
4195
|
if (scheduledAt !== void 0) body.scheduleDate = scheduledAt;
|
|
4124
4196
|
if (tags !== void 0) body.tags = tags;
|
|
4125
4197
|
if (ogData !== void 0) body.ogData = ogData;
|
|
@@ -6007,13 +6079,34 @@ ${text2}`);
|
|
|
6007
6079
|
|
|
6008
6080
|
// src/tools/form-builder.ts
|
|
6009
6081
|
var import_zod35 = require("zod");
|
|
6082
|
+
function pickFormName(builderForm) {
|
|
6083
|
+
if (builderForm && typeof builderForm === "object" && "form" in builderForm) {
|
|
6084
|
+
const name = builderForm.form?.name;
|
|
6085
|
+
if (typeof name === "string" && name.length > 0) return name;
|
|
6086
|
+
}
|
|
6087
|
+
return void 0;
|
|
6088
|
+
}
|
|
6089
|
+
function findFormNameInList(list, formId) {
|
|
6090
|
+
const forms = list?.forms;
|
|
6091
|
+
if (Array.isArray(forms)) {
|
|
6092
|
+
for (const f of forms) {
|
|
6093
|
+
if (f && typeof f === "object") {
|
|
6094
|
+
const entry = f;
|
|
6095
|
+
if ((entry.id === formId || entry._id === formId) && typeof entry.name === "string" && entry.name.length > 0) {
|
|
6096
|
+
return entry.name;
|
|
6097
|
+
}
|
|
6098
|
+
}
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
return void 0;
|
|
6102
|
+
}
|
|
6010
6103
|
function buildUpdateFormPath(formId, locationId2) {
|
|
6011
6104
|
return `/${formId}?locationId=${locationId2}`;
|
|
6012
6105
|
}
|
|
6013
6106
|
function buildUpdateFormBody(name, formData) {
|
|
6014
6107
|
return { name, formData };
|
|
6015
6108
|
}
|
|
6016
|
-
function registerFormBuilderTools(server2, builderClient) {
|
|
6109
|
+
function registerFormBuilderTools(server2, builderClient, publicClient) {
|
|
6017
6110
|
const client = builderClient;
|
|
6018
6111
|
if (!client) return;
|
|
6019
6112
|
async function formRequest(method, path7, body) {
|
|
@@ -6067,11 +6160,28 @@ ${text2}`);
|
|
|
6067
6160
|
let resolvedName = name;
|
|
6068
6161
|
if (resolvedName === void 0) {
|
|
6069
6162
|
const current = await formRequest("GET", `/${formId}?locationId=${client.locationId}`);
|
|
6070
|
-
|
|
6071
|
-
if (
|
|
6072
|
-
|
|
6163
|
+
resolvedName = pickFormName(current);
|
|
6164
|
+
if (resolvedName === void 0 && publicClient) {
|
|
6165
|
+
try {
|
|
6166
|
+
const pageSize = 100;
|
|
6167
|
+
const maxForms = 5e3;
|
|
6168
|
+
for (let skip = 0; skip < maxForms; skip += pageSize) {
|
|
6169
|
+
const list = await publicClient.get("/forms/", {
|
|
6170
|
+
params: { locationId: client.locationId, limit: pageSize, skip }
|
|
6171
|
+
});
|
|
6172
|
+
resolvedName = findFormNameInList(list, formId);
|
|
6173
|
+
if (resolvedName !== void 0) break;
|
|
6174
|
+
const forms = list?.forms;
|
|
6175
|
+
if (!Array.isArray(forms) || forms.length < pageSize) break;
|
|
6176
|
+
}
|
|
6177
|
+
} catch {
|
|
6178
|
+
}
|
|
6179
|
+
}
|
|
6180
|
+
if (resolvedName === void 0) {
|
|
6181
|
+
throw new Error(
|
|
6182
|
+
`Could not resolve the current name for form ${formId}. Pass the \`name\` argument explicitly \u2014 a form created via create_form has no builder-doc name until its first save.`
|
|
6183
|
+
);
|
|
6073
6184
|
}
|
|
6074
|
-
resolvedName = currentName;
|
|
6075
6185
|
}
|
|
6076
6186
|
const result = await formRequest(
|
|
6077
6187
|
"POST",
|
|
@@ -9545,7 +9655,6 @@ var publicApiTools = [
|
|
|
9545
9655
|
var internalApiTools = [
|
|
9546
9656
|
[registerWorkflowBuilderTools, "workflow-builder"],
|
|
9547
9657
|
[registerFunnelBuilderTools, "funnel-builder"],
|
|
9548
|
-
[registerFormBuilderTools, "form-builder"],
|
|
9549
9658
|
[registerPipelineBuilderTools, "pipeline-builder"],
|
|
9550
9659
|
[registerWorkflowClonerTools, "workflow-cloner"],
|
|
9551
9660
|
[registerSmartListTools, "smart-lists"],
|
|
@@ -9558,9 +9667,11 @@ var VALIDATORS_MODULE = "validators";
|
|
|
9558
9667
|
var DIAGNOSTICS_MODULE = "diagnostics";
|
|
9559
9668
|
var LOCATION_SWITCHER_MODULE = "location-switcher";
|
|
9560
9669
|
var SNAPSHOTS_MODULE = "snapshots";
|
|
9670
|
+
var FORM_BUILDER_MODULE = "form-builder";
|
|
9561
9671
|
var KNOWN_MODULES = /* @__PURE__ */ new Set([
|
|
9562
9672
|
...publicApiTools.map(([, label]) => label),
|
|
9563
9673
|
...internalApiTools.map(([, label]) => label),
|
|
9674
|
+
FORM_BUILDER_MODULE,
|
|
9564
9675
|
VALIDATORS_MODULE,
|
|
9565
9676
|
DIAGNOSTICS_MODULE,
|
|
9566
9677
|
LOCATION_SWITCHER_MODULE,
|
|
@@ -9578,6 +9689,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
|
|
|
9578
9689
|
for (const [register, moduleName] of internalApiTools) {
|
|
9579
9690
|
register(wrap(moduleName), builderClient);
|
|
9580
9691
|
}
|
|
9692
|
+
registerFormBuilderTools(wrap(FORM_BUILDER_MODULE), builderClient, client);
|
|
9581
9693
|
registerValidatorTools(wrap(VALIDATORS_MODULE), client, builderClient);
|
|
9582
9694
|
registerDiagnosticTools(
|
|
9583
9695
|
wrap(DIAGNOSTICS_MODULE),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.35.0",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
5
|
"description": "GoHighLevel MCP Server for Claude. 218 tools — full CRM, automation, marketing control, account-wide workflow audit, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
|
|
6
6
|
"main": "dist/index.js",
|