@elitedcs/ghl-mcp 3.37.0 → 3.38.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/dist/index.js +131 -10
- package/package.json +1 -1
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.38.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",
|
|
@@ -10851,6 +10851,35 @@ var WAIT_UNIT_MAP = {
|
|
|
10851
10851
|
};
|
|
10852
10852
|
var PENDING = (ref) => `__PENDING__:${ref}`;
|
|
10853
10853
|
var isPending = (v) => v.startsWith("__PENDING__:");
|
|
10854
|
+
var STAFF_CALENDAR_TYPES = /* @__PURE__ */ new Set([
|
|
10855
|
+
"round_robin",
|
|
10856
|
+
"collective",
|
|
10857
|
+
"class_booking",
|
|
10858
|
+
"service_booking"
|
|
10859
|
+
]);
|
|
10860
|
+
function calendarNeedsStaff(cal) {
|
|
10861
|
+
return cal.requiresStaff === true || STAFF_CALENDAR_TYPES.has(cal.calendarType);
|
|
10862
|
+
}
|
|
10863
|
+
function classifyCalendarBuild(cal, userCount) {
|
|
10864
|
+
if (!calendarNeedsStaff(cal)) return { action: "build" };
|
|
10865
|
+
if (userCount === 1) return { action: "build" };
|
|
10866
|
+
if (userCount === void 0) {
|
|
10867
|
+
return {
|
|
10868
|
+
action: "manual",
|
|
10869
|
+
reason: `Calendar "${cal.name}" (${cal.calendarType}) needs a team member, but Blueprint couldn't read this account's users to auto-assign one. Assign the booking staff to it in the GHL UI (or build it there), then re-run to bind it.`
|
|
10870
|
+
};
|
|
10871
|
+
}
|
|
10872
|
+
if (userCount === 0) {
|
|
10873
|
+
return {
|
|
10874
|
+
action: "manual",
|
|
10875
|
+
reason: `Calendar "${cal.name}" (${cal.calendarType}) needs a team member, but this account has no users yet. Add a user and assign them to it, then re-run to build it.`
|
|
10876
|
+
};
|
|
10877
|
+
}
|
|
10878
|
+
return {
|
|
10879
|
+
action: "manual",
|
|
10880
|
+
reason: `Calendar "${cal.name}" (${cal.calendarType}) needs a team member, and this account has ${userCount} users \u2014 Blueprint won't guess which one books. Assign the booking staff to it in the GHL UI (or build it there), then re-run to bind it.`
|
|
10881
|
+
};
|
|
10882
|
+
}
|
|
10854
10883
|
var norm = (s) => s.trim().toLowerCase();
|
|
10855
10884
|
function buildRefIndex(plan) {
|
|
10856
10885
|
const idx = {
|
|
@@ -11192,6 +11221,15 @@ function resolvePlan(plan, existing, opts = {}) {
|
|
|
11192
11221
|
}
|
|
11193
11222
|
}
|
|
11194
11223
|
}
|
|
11224
|
+
const existingCalNames = new Set((existing.calendars ?? []).map((c) => norm(c.name)));
|
|
11225
|
+
const calendarsManual = [];
|
|
11226
|
+
for (const cal of plan.calendars ?? []) {
|
|
11227
|
+
if (existingCalNames.has(norm(cal.name))) continue;
|
|
11228
|
+
const decision = classifyCalendarBuild(cal, existing.userCount);
|
|
11229
|
+
if (decision.action === "manual") {
|
|
11230
|
+
calendarsManual.push({ ref: cal.ref, name: cal.name, reason: decision.reason ?? "needs a team member" });
|
|
11231
|
+
}
|
|
11232
|
+
}
|
|
11195
11233
|
const gates = computeWorkflowGates(plan, metHandoffs);
|
|
11196
11234
|
const workflows = (plan.workflows ?? []).map((w) => expandWorkflow(w, idx, idMap, gates.get(w.ref) ?? []));
|
|
11197
11235
|
const handoffs = (plan.handoffs ?? []).map((h) => ({
|
|
@@ -11203,8 +11241,9 @@ function resolvePlan(plan, existing, opts = {}) {
|
|
|
11203
11241
|
met: metHandoffs.has(h.ref),
|
|
11204
11242
|
blocks: h.blocks ?? []
|
|
11205
11243
|
}));
|
|
11244
|
+
const manualCalRefs = new Set(calendarsManual.map((c) => c.ref));
|
|
11206
11245
|
const summary = {
|
|
11207
|
-
wouldCreate: items.filter((i) => i.status === "would_create").length,
|
|
11246
|
+
wouldCreate: items.filter((i) => i.status === "would_create" && !manualCalRefs.has(i.ref)).length,
|
|
11208
11247
|
existing: items.filter((i) => i.status === "existing").length,
|
|
11209
11248
|
workflowsTotal: workflows.length,
|
|
11210
11249
|
workflowsGated: workflows.filter((w) => w.gatedBy.length > 0).length,
|
|
@@ -11212,7 +11251,7 @@ function resolvePlan(plan, existing, opts = {}) {
|
|
|
11212
11251
|
actionsManual: workflows.reduce((n, w) => n + w.manual.length, 0),
|
|
11213
11252
|
actionsNeedContent: workflows.reduce((n, w) => n + w.needsContent.length, 0)
|
|
11214
11253
|
};
|
|
11215
|
-
return { items, idMap: Object.fromEntries(idMap), workflows, handoffs, summary };
|
|
11254
|
+
return { items, idMap: Object.fromEntries(idMap), workflows, calendarsManual, handoffs, summary };
|
|
11216
11255
|
}
|
|
11217
11256
|
function renderReport(plan, result, ctx) {
|
|
11218
11257
|
const L = [];
|
|
@@ -11228,11 +11267,13 @@ function renderReport(plan, result, ctx) {
|
|
|
11228
11267
|
`Workflows: ${summary.workflowsTotal} (${summary.workflowsGated} gated DRAFT by an unmet handoff). Actions: ${summary.actionsExpanded} auto-built, ${summary.actionsManual} need a manual GHL-UI step, ${summary.actionsNeedContent} need send-ready copy.`
|
|
11229
11268
|
);
|
|
11230
11269
|
L.push("");
|
|
11270
|
+
const manualCalRefs = new Set(result.calendarsManual.map((c) => c.ref));
|
|
11231
11271
|
L.push("\u2500\u2500 Blueprint builds automatically \u2500\u2500");
|
|
11232
11272
|
for (const section2 of EXECUTION_ORDER) {
|
|
11233
11273
|
const secItems = result.items.filter((i) => i.type === section2);
|
|
11234
11274
|
if (secItems.length === 0) continue;
|
|
11235
11275
|
for (const it of secItems) {
|
|
11276
|
+
if (manualCalRefs.has(it.ref)) continue;
|
|
11236
11277
|
const mark = it.status === "existing" ? "skip (exists)" : ctx.mode === "dry_run" ? "would create" : "create";
|
|
11237
11278
|
L.push(` [${section2}] ${it.name} \u2014 ${mark}${it.existingId ? ` \u2192 ${it.existingId}` : ""}`);
|
|
11238
11279
|
}
|
|
@@ -11245,6 +11286,10 @@ function renderReport(plan, result, ctx) {
|
|
|
11245
11286
|
L.push("");
|
|
11246
11287
|
L.push("\u2500\u2500 You must do these by hand (in order) \u2500\u2500");
|
|
11247
11288
|
let any = false;
|
|
11289
|
+
for (const c of result.calendarsManual) {
|
|
11290
|
+
any = true;
|
|
11291
|
+
L.push(` \u2022 [calendar] ${c.reason}`);
|
|
11292
|
+
}
|
|
11248
11293
|
for (const w of result.workflows) {
|
|
11249
11294
|
for (const m of w.manual) {
|
|
11250
11295
|
any = true;
|
|
@@ -11272,10 +11317,12 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
11272
11317
|
const backoff = opts.verifyBackoffMs ?? 500;
|
|
11273
11318
|
const idMap = {};
|
|
11274
11319
|
const built = [];
|
|
11320
|
+
const manual = [];
|
|
11275
11321
|
const halt = (atRef, type, reason) => ({
|
|
11276
11322
|
ok: false,
|
|
11277
11323
|
idMap,
|
|
11278
11324
|
built,
|
|
11325
|
+
manual,
|
|
11279
11326
|
halted: { atRef, type, reason },
|
|
11280
11327
|
deferred: deferredSections(plan)
|
|
11281
11328
|
});
|
|
@@ -11363,7 +11410,62 @@ async function executeBackbone(plan, deps, opts = {}) {
|
|
|
11363
11410
|
built
|
|
11364
11411
|
);
|
|
11365
11412
|
if (cvHalt) return halt(cvHalt.ref, "cv", cvHalt.reason);
|
|
11366
|
-
|
|
11413
|
+
let cachedUserCount;
|
|
11414
|
+
let cachedSoloUserId;
|
|
11415
|
+
let usersRead = false;
|
|
11416
|
+
for (const cal of plan.calendars ?? []) {
|
|
11417
|
+
let calendars;
|
|
11418
|
+
try {
|
|
11419
|
+
calendars = await deps.listCalendars();
|
|
11420
|
+
} catch (e) {
|
|
11421
|
+
return halt(cal.ref, "calendar", `could not read existing calendars: ${msg(e)}`);
|
|
11422
|
+
}
|
|
11423
|
+
const matches = calendars.filter((c) => norm2(c.name) === norm2(cal.name));
|
|
11424
|
+
if (matches.length > 1) {
|
|
11425
|
+
return halt(cal.ref, "calendar", `${matches.length} existing calendars are named "${cal.name}" \u2014 ambiguous, cannot safely bind ${cal.ref}. Resolve the duplicate in GHL, then re-run.`);
|
|
11426
|
+
}
|
|
11427
|
+
if (matches.length === 1) {
|
|
11428
|
+
idMap[cal.ref] = matches[0].id;
|
|
11429
|
+
built.push({ ref: cal.ref, type: "calendar", name: cal.name, status: "existing", realId: matches[0].id });
|
|
11430
|
+
continue;
|
|
11431
|
+
}
|
|
11432
|
+
let staffUserId;
|
|
11433
|
+
if (calendarNeedsStaff(cal)) {
|
|
11434
|
+
if (!usersRead) {
|
|
11435
|
+
try {
|
|
11436
|
+
const users = await deps.listUsers();
|
|
11437
|
+
cachedUserCount = users.length;
|
|
11438
|
+
cachedSoloUserId = users.length === 1 ? users[0].id : void 0;
|
|
11439
|
+
} catch {
|
|
11440
|
+
cachedUserCount = void 0;
|
|
11441
|
+
}
|
|
11442
|
+
usersRead = true;
|
|
11443
|
+
}
|
|
11444
|
+
const decision = classifyCalendarBuild(cal, cachedUserCount);
|
|
11445
|
+
if (decision.action === "manual") {
|
|
11446
|
+
manual.push({ ref: cal.ref, type: "calendar", name: cal.name, reason: decision.reason ?? "needs a team member" });
|
|
11447
|
+
continue;
|
|
11448
|
+
}
|
|
11449
|
+
staffUserId = cachedSoloUserId;
|
|
11450
|
+
}
|
|
11451
|
+
const beforeIds = new Set(calendars.map((c) => c.id));
|
|
11452
|
+
try {
|
|
11453
|
+
await deps.createCalendar({
|
|
11454
|
+
name: cal.name,
|
|
11455
|
+
calendarType: cal.calendarType,
|
|
11456
|
+
openHours: cal.openHours,
|
|
11457
|
+
availabilityType: cal.availabilityType,
|
|
11458
|
+
staffUserId
|
|
11459
|
+
});
|
|
11460
|
+
} catch (e) {
|
|
11461
|
+
return halt(cal.ref, "calendar", `create failed: ${msg(e)}`);
|
|
11462
|
+
}
|
|
11463
|
+
const verified = await pollForNew(() => deps.listCalendars(), cal.name, beforeIds);
|
|
11464
|
+
if (!verified) return halt(cal.ref, "calendar", "created but could not verify a single new calendar by read-back (none, or an ambiguous duplicate, appeared)");
|
|
11465
|
+
idMap[cal.ref] = verified.id;
|
|
11466
|
+
built.push({ ref: cal.ref, type: "calendar", name: cal.name, status: "created", realId: verified.id });
|
|
11467
|
+
}
|
|
11468
|
+
return { ok: true, idMap, built, manual, deferred: deferredSections(plan) };
|
|
11367
11469
|
}
|
|
11368
11470
|
async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMap, built) {
|
|
11369
11471
|
if (objects.length === 0) return null;
|
|
@@ -11399,7 +11501,6 @@ async function buildSimple(objects, type, list, create, nameOf, pollForNew, idMa
|
|
|
11399
11501
|
}
|
|
11400
11502
|
function deferredSections(plan) {
|
|
11401
11503
|
const out = [];
|
|
11402
|
-
if (plan.calendars?.length) out.push({ section: "calendars", count: plan.calendars.length });
|
|
11403
11504
|
if (plan.forms?.length) out.push({ section: "forms", count: plan.forms.length });
|
|
11404
11505
|
if (plan.funnels?.length) out.push({ section: "funnels", count: plan.funnels.length });
|
|
11405
11506
|
if (plan.workflows?.length) out.push({ section: "workflows", count: plan.workflows.length });
|
|
@@ -11511,6 +11612,11 @@ async function scanExistingAssets(client, locationId2) {
|
|
|
11511
11612
|
await read("forms", async () => pickObjects(await client.get("/forms/", { params: { locationId: locationId2, limit: 100 } }), ["forms"]), "forms");
|
|
11512
11613
|
await read("funnels", async () => pickObjects(await client.get("/funnels/funnel/list", { params: { locationId: locationId2, limit: 100 } }), ["funnels"]), "funnels");
|
|
11513
11614
|
await read("workflows", async () => pickObjects(await client.get("/workflows/", { params: { locationId: locationId2 } }), ["workflows"]), "workflows");
|
|
11615
|
+
try {
|
|
11616
|
+
assets.userCount = pickObjects(await client.get("/users/", { params: { locationId: locationId2 } }), ["users"]).length;
|
|
11617
|
+
} catch (e) {
|
|
11618
|
+
warnings.push(`could not scan users (staff-requiring calendars will defer to a manual step): ${e instanceof Error ? e.message : String(e)}`);
|
|
11619
|
+
}
|
|
11514
11620
|
return { assets, warnings };
|
|
11515
11621
|
}
|
|
11516
11622
|
function pickPipelines(raw) {
|
|
@@ -11582,6 +11688,18 @@ ${text2.slice(0, 300)}`);
|
|
|
11582
11688
|
listCustomValues: async () => pickObjects(await client.get(`/locations/${locationId2}/customValues`), ["customValues"]),
|
|
11583
11689
|
createCustomValue: async (name, value) => {
|
|
11584
11690
|
await client.post(`/locations/${locationId2}/customValues`, { body: { name, value }, noRetry: true });
|
|
11691
|
+
},
|
|
11692
|
+
listCalendars: async () => pickObjects(await client.get("/calendars/", { params: { locationId: locationId2 } }), ["calendars"]),
|
|
11693
|
+
// Users: count + the lone id (when solo) drive auto-staff. The /users/
|
|
11694
|
+
// endpoint 422s on a `limit` param under PIT auth, so pass only locationId.
|
|
11695
|
+
listUsers: async () => pickObjects(await client.get("/users/", { params: { locationId: locationId2 } }), ["users"]),
|
|
11696
|
+
createCalendar: async (cal) => {
|
|
11697
|
+
const teamMembers = cal.staffUserId ? [{ userId: cal.staffUserId, priority: 1, isPrimary: true, selected: true, locationConfigurations: [{ kind: "custom", position: 0 }] }] : void 0;
|
|
11698
|
+
const body = buildCreateCalendarBody(
|
|
11699
|
+
{ name: cal.name, calendarType: cal.calendarType, openHours: cal.openHours, availabilityType: cal.availabilityType, teamMembers },
|
|
11700
|
+
locationId2
|
|
11701
|
+
);
|
|
11702
|
+
await client.post("/calendars/", { body, noRetry: true });
|
|
11585
11703
|
}
|
|
11586
11704
|
};
|
|
11587
11705
|
}
|
|
@@ -11640,7 +11758,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11640
11758
|
);
|
|
11641
11759
|
server2.tool(
|
|
11642
11760
|
"apply_build_plan",
|
|
11643
|
-
`Take an APPROVED Blueprint \xA75 build plan + the CURRENT confirmed sub-account and build it. mode:"dry_run" (default) writes NOTHING \u2014 it resolves refs, expands each workflow's logical actions to native GHL JSON, runs the NEVER-CLOBBER existing-asset scan, and returns a two-part report. Run it FIRST. mode:"execute" performs LIVE writes for the CRM backbone (pipelines+stages, custom fields, tags, custom values): never clobbers (same-named objects are bound to their existing id, never modified), verifies each create by read-back before resolving its ref, halts on the first failure returning the partial idMap, and is idempotent (re-run = no-op).
|
|
11761
|
+
`Take an APPROVED Blueprint \xA75 build plan + the CURRENT confirmed sub-account and build it. mode:"dry_run" (default) writes NOTHING \u2014 it resolves refs, expands each workflow's logical actions to native GHL JSON, runs the NEVER-CLOBBER existing-asset scan, and returns a two-part report. Run it FIRST. mode:"execute" performs LIVE writes for the CRM backbone (pipelines+stages, custom fields, tags, custom values) AND calendars: never clobbers (same-named objects are bound to their existing id, never modified), verifies each create by read-back before resolving its ref, halts on the first failure returning the partial idMap, and is idempotent (re-run = no-op). Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole account user (auto-assigns you as the team member); with 0 or 2+ users they're surfaced as a manual step, not auto-staffed to a guess. Forms/funnels/workflows are surfaced as manual next steps, not auto-built yet. Always confirms the active location and validates the plan before any write.`,
|
|
11644
11762
|
{
|
|
11645
11763
|
plan: import_zod53.z.record(import_zod53.z.unknown()).describe("The approved \xA75 Build Plan object."),
|
|
11646
11764
|
mode: import_zod53.z.enum(["dry_run", "execute"]).optional().describe("dry_run (default) = resolve/expand/scan/report, no writes. execute = live writes (not yet enabled)."),
|
|
@@ -11715,6 +11833,7 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11715
11833
|
}
|
|
11716
11834
|
const deps = makeExecuteDeps(client, builderClient, activeLocation);
|
|
11717
11835
|
const exec = await executeBackbone(typedPlan, deps);
|
|
11836
|
+
const calendarManualLines = exec.manual.map((m) => `[calendar] ${m.reason}`);
|
|
11718
11837
|
const manualLines = result.workflows.flatMap((w) => [
|
|
11719
11838
|
...w.manual.map((m) => `[${w.name}] ${m.reason}`),
|
|
11720
11839
|
...w.needsContent.map((c) => `[${w.name}] ${c.reason}`)
|
|
@@ -11729,11 +11848,12 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11729
11848
|
scanWarnings,
|
|
11730
11849
|
halted: exec.halted,
|
|
11731
11850
|
built: exec.built,
|
|
11851
|
+
manual: exec.manual,
|
|
11732
11852
|
idMap: exec.idMap,
|
|
11733
11853
|
deferred: exec.deferred,
|
|
11734
|
-
deferredNote: "
|
|
11735
|
-
nextManualSteps: [...manualLines, ...handoffLines],
|
|
11736
|
-
summary: exec.ok ? `Built ${exec.built.filter((b) => b.status === "created").length} new object(s), bound ${exec.built.filter((b) => b.status === "existing").length} existing. ${exec.deferred.length ? "Deferred: " + exec.deferred.map((d) => `${d.count} ${d.section}`).join(", ") + " (manual)." : ""}` : `HALTED at ${exec.halted?.atRef} (${exec.halted?.reason}). ${exec.built.length} object(s) were created before the halt \u2014 see idMap to resume or clean up. NEVER-CLOBBER means a re-run will bind those, not duplicate them.`
|
|
11854
|
+
deferredNote: "execute builds the CRM backbone (pipelines, custom fields, tags, custom values) and calendars live. Staff-requiring calendars (round_robin etc.) auto-build only when you are the sole user; otherwise they're listed under manual steps. Forms/funnels/workflows are planned but NOT auto-built yet \u2014 create them via the GHL UI or the dedicated tools, in this order: forms \u2192 funnels \u2192 workflows.",
|
|
11855
|
+
nextManualSteps: [...calendarManualLines, ...manualLines, ...handoffLines],
|
|
11856
|
+
summary: exec.ok ? `Built ${exec.built.filter((b) => b.status === "created").length} new object(s), bound ${exec.built.filter((b) => b.status === "existing").length} existing.${exec.manual.length ? ` ${exec.manual.length} calendar(s) need manual staff assignment.` : ""}${exec.deferred.length ? " Deferred: " + exec.deferred.map((d) => `${d.count} ${d.section}`).join(", ") + " (manual)." : ""}` : `HALTED at ${exec.halted?.atRef} (${exec.halted?.reason}). ${exec.built.length} object(s) were created before the halt \u2014 see idMap to resume or clean up. NEVER-CLOBBER means a re-run will bind those, not duplicate them.`
|
|
11737
11857
|
});
|
|
11738
11858
|
}
|
|
11739
11859
|
const collisions = result.items.filter((i) => i.status === "existing");
|
|
@@ -11768,9 +11888,10 @@ function registerIntakeToBuildTools(server2, client, builderClient) {
|
|
|
11768
11888
|
gatedBy: w.gatedBy,
|
|
11769
11889
|
draft: w.gatedBy.length > 0 || !(publishWorkflows ?? false)
|
|
11770
11890
|
})),
|
|
11891
|
+
calendarsManual: result.calendarsManual,
|
|
11771
11892
|
handoffs: result.handoffs,
|
|
11772
11893
|
report,
|
|
11773
|
-
next: 'Review the report. When it looks right, re-run with mode:"execute" to build the CRM backbone live (pipelines, fields, tags, custom values).
|
|
11894
|
+
next: 'Review the report. When it looks right, re-run with mode:"execute" to build the CRM backbone + calendars live (pipelines, fields, tags, custom values, calendars). Forms/funnels/workflows are listed as manual next steps.'
|
|
11774
11895
|
});
|
|
11775
11896
|
} catch (error) {
|
|
11776
11897
|
return errorResponse(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.38.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",
|