@elitedcs/ghl-mcp 3.34.9 → 3.34.10

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/index.js +105 -12
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.34.10 — `get_free_slots` accepts ISO dates; `update_form` name fallback for fresh forms
4
+
5
+ Two small fixes that close rough edges found while live-verifying v3.34.9 through
6
+ the MCP tools. Neither changes a proven capability; both have trivial workarounds.
7
+
8
+ - **`get_free_slots` now accepts ISO dates (its documented input) — not just epoch
9
+ milliseconds.** GHL's `GET /calendars/{id}/free-slots` requires `startDate` /
10
+ `endDate` as epoch **milliseconds** and 422s (`startDate must be a number…`) on an
11
+ ISO date, yet the tool's own schema documented `YYYY-MM-DD` and forwarded it
12
+ unconverted — so the documented input always failed. The tool now accepts a bare
13
+ `YYYY-MM-DD` (taken as start-of-day for `startDate`, end-of-day for `endDate`, in
14
+ the supplied `timezone`, default UTC), a full ISO datetime (`Date.parse`), or an
15
+ already-numeric epoch-millis value (passed through, back-compat) and converts to
16
+ the epoch ms GHL needs. Logic extracted to `buildFreeSlotsParams` / `toEpochMillis`
17
+ with tests (incl. the live Phoenix anchor `2026-06-15 → 1781506800000`).
18
+ - **`update_form` no longer throws on the natural `create_form` → `update_form`
19
+ sequence.** When `name` is omitted, `update_form` reads the current name to
20
+ preserve it. A form freshly created by `create_form` has no `name` in its builder
21
+ doc until its first save, so that read returned none and the call threw
22
+ (`Could not resolve current form name`). It now falls back to the public forms list
23
+ (which carries the name) and only errors — with actionable guidance to pass `name`
24
+ — if both sources come up empty. The form-builder tools now also receive the public
25
+ `GHLClient` for this lookup (it already had Firebase auth for the builder routes).
26
+ Helpers `pickFormName` / `findFormNameInList` extracted, with tests.
27
+
3
28
  ## 3.34.9 — Calendar availability: create_calendar / update_calendar now set `openHours`
4
29
 
5
30
  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.9",
34
+ version: "3.34.10",
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 date in ISO format (YYYY-MM-DD)"),
2885
- endDate: import_zod9.z.string().describe("End date in ISO format (YYYY-MM-DD)"),
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
  });
@@ -6007,13 +6060,34 @@ ${text2}`);
6007
6060
 
6008
6061
  // src/tools/form-builder.ts
6009
6062
  var import_zod35 = require("zod");
6063
+ function pickFormName(builderForm) {
6064
+ if (builderForm && typeof builderForm === "object" && "form" in builderForm) {
6065
+ const name = builderForm.form?.name;
6066
+ if (typeof name === "string" && name.length > 0) return name;
6067
+ }
6068
+ return void 0;
6069
+ }
6070
+ function findFormNameInList(list, formId) {
6071
+ const forms = list?.forms;
6072
+ if (Array.isArray(forms)) {
6073
+ for (const f of forms) {
6074
+ if (f && typeof f === "object") {
6075
+ const entry = f;
6076
+ if ((entry.id === formId || entry._id === formId) && typeof entry.name === "string" && entry.name.length > 0) {
6077
+ return entry.name;
6078
+ }
6079
+ }
6080
+ }
6081
+ }
6082
+ return void 0;
6083
+ }
6010
6084
  function buildUpdateFormPath(formId, locationId2) {
6011
6085
  return `/${formId}?locationId=${locationId2}`;
6012
6086
  }
6013
6087
  function buildUpdateFormBody(name, formData) {
6014
6088
  return { name, formData };
6015
6089
  }
6016
- function registerFormBuilderTools(server2, builderClient) {
6090
+ function registerFormBuilderTools(server2, builderClient, publicClient) {
6017
6091
  const client = builderClient;
6018
6092
  if (!client) return;
6019
6093
  async function formRequest(method, path7, body) {
@@ -6067,11 +6141,28 @@ ${text2}`);
6067
6141
  let resolvedName = name;
6068
6142
  if (resolvedName === void 0) {
6069
6143
  const current = await formRequest("GET", `/${formId}?locationId=${client.locationId}`);
6070
- const currentName = typeof current === "object" && current !== null && "form" in current ? current.form?.name : void 0;
6071
- if (typeof currentName !== "string") {
6072
- throw new Error(`Could not resolve current form name for ${formId} (GET /forms/${formId} returned no string name).`);
6144
+ resolvedName = pickFormName(current);
6145
+ if (resolvedName === void 0 && publicClient) {
6146
+ try {
6147
+ const pageSize = 100;
6148
+ const maxForms = 5e3;
6149
+ for (let skip = 0; skip < maxForms; skip += pageSize) {
6150
+ const list = await publicClient.get("/forms/", {
6151
+ params: { locationId: client.locationId, limit: pageSize, skip }
6152
+ });
6153
+ resolvedName = findFormNameInList(list, formId);
6154
+ if (resolvedName !== void 0) break;
6155
+ const forms = list?.forms;
6156
+ if (!Array.isArray(forms) || forms.length < pageSize) break;
6157
+ }
6158
+ } catch {
6159
+ }
6160
+ }
6161
+ if (resolvedName === void 0) {
6162
+ throw new Error(
6163
+ `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.`
6164
+ );
6073
6165
  }
6074
- resolvedName = currentName;
6075
6166
  }
6076
6167
  const result = await formRequest(
6077
6168
  "POST",
@@ -9545,7 +9636,6 @@ var publicApiTools = [
9545
9636
  var internalApiTools = [
9546
9637
  [registerWorkflowBuilderTools, "workflow-builder"],
9547
9638
  [registerFunnelBuilderTools, "funnel-builder"],
9548
- [registerFormBuilderTools, "form-builder"],
9549
9639
  [registerPipelineBuilderTools, "pipeline-builder"],
9550
9640
  [registerWorkflowClonerTools, "workflow-cloner"],
9551
9641
  [registerSmartListTools, "smart-lists"],
@@ -9558,9 +9648,11 @@ var VALIDATORS_MODULE = "validators";
9558
9648
  var DIAGNOSTICS_MODULE = "diagnostics";
9559
9649
  var LOCATION_SWITCHER_MODULE = "location-switcher";
9560
9650
  var SNAPSHOTS_MODULE = "snapshots";
9651
+ var FORM_BUILDER_MODULE = "form-builder";
9561
9652
  var KNOWN_MODULES = /* @__PURE__ */ new Set([
9562
9653
  ...publicApiTools.map(([, label]) => label),
9563
9654
  ...internalApiTools.map(([, label]) => label),
9655
+ FORM_BUILDER_MODULE,
9564
9656
  VALIDATORS_MODULE,
9565
9657
  DIAGNOSTICS_MODULE,
9566
9658
  LOCATION_SWITCHER_MODULE,
@@ -9578,6 +9670,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
9578
9670
  for (const [register, moduleName] of internalApiTools) {
9579
9671
  register(wrap(moduleName), builderClient);
9580
9672
  }
9673
+ registerFormBuilderTools(wrap(FORM_BUILDER_MODULE), builderClient, client);
9581
9674
  registerValidatorTools(wrap(VALIDATORS_MODULE), client, builderClient);
9582
9675
  registerDiagnosticTools(
9583
9676
  wrap(DIAGNOSTICS_MODULE),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.34.9",
3
+ "version": "3.34.10",
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",