@elitedcs/ghl-mcp 3.28.0 → 3.30.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 +66 -0
- package/dist/index.js +222 -57
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.30.0 — Workflow trigger stays active after edit/publish (Bug 7)
|
|
4
|
+
|
|
5
|
+
Editing a trigger on a published workflow via `update_workflow_actions` silently
|
|
6
|
+
disabled it (`active` flipped to `false`), and there was no MCP path to turn it
|
|
7
|
+
back on. Any contract/tag/form trigger you edited stopped firing.
|
|
8
|
+
|
|
9
|
+
**Root cause.** The internal trigger-write helper hardcoded `status: "draft"` on
|
|
10
|
+
every trigger POST/PUT. GHL derives a trigger's stored `active` flag from that
|
|
11
|
+
write-time `status`. Verified live against the sandbox:
|
|
12
|
+
|
|
13
|
+
- `status:"published"` → `active:true`
|
|
14
|
+
- `status:"draft"` → `active:false` (and so do `"active"`, `"live"`, and
|
|
15
|
+
omitting `status` entirely)
|
|
16
|
+
|
|
17
|
+
So every trigger edit re-drafted the trigger, and nothing flipped it back —
|
|
18
|
+
`publish_workflow` didn't either, because it sent no triggers at all.
|
|
19
|
+
|
|
20
|
+
**Fix.** Trigger writes now mirror the workflow's own published/draft state:
|
|
21
|
+
|
|
22
|
+
- `update_workflow_actions` on a published workflow writes its triggers as
|
|
23
|
+
`published`, so editing a trigger condition keeps it live.
|
|
24
|
+
- `publish_workflow` now re-syncs the workflow's triggers, so a draft →
|
|
25
|
+
published transition (including the documented create → add-trigger →
|
|
26
|
+
publish flow) activates them.
|
|
27
|
+
|
|
28
|
+
Action-chain linking and the workflow-setting preservation from 3.29.0 are
|
|
29
|
+
unchanged.
|
|
30
|
+
|
|
31
|
+
## 3.29.0 — Documents & Contracts API fix + workflow/contact bug fixes
|
|
32
|
+
|
|
33
|
+
Five confirmed bugs fixed, verified live against the GHL API.
|
|
34
|
+
|
|
35
|
+
**Documents & Contracts (the big one).** The document tools were hitting a
|
|
36
|
+
non-existent `/documents/` path and returning 404. GHL's real Documents &
|
|
37
|
+
Contracts API lives under `/proposals/*`:
|
|
38
|
+
|
|
39
|
+
- `list_documents` now calls `GET /proposals/document` with real filters:
|
|
40
|
+
`status` (draft/sent/viewed/completed/declined), `paymentStatus`, `query`,
|
|
41
|
+
`dateFrom`/`dateTo`, and pagination (`limit` capped at GHL's max of 21, `skip`).
|
|
42
|
+
- `get_document` resolves a document by id by scanning the list (GHL exposes no
|
|
43
|
+
public get-by-id route).
|
|
44
|
+
- `send_document` now calls `POST /proposals/document/send` to dispatch an
|
|
45
|
+
existing document to its recipients.
|
|
46
|
+
- `delete_document` reports honestly that GHL's public API has no delete/void
|
|
47
|
+
route (do it in the UI) instead of failing on a dead path.
|
|
48
|
+
- **New:** `list_document_templates` (`GET /proposals/templates`) lists your
|
|
49
|
+
reusable contract templates.
|
|
50
|
+
- **New:** `send_document_template` (`POST /proposals/templates/send`) creates
|
|
51
|
+
and sends a contract to a contact from a template.
|
|
52
|
+
|
|
53
|
+
**Workflow triggers.** `get_workflow_full` and `update_workflow_actions` no
|
|
54
|
+
longer crash on the Documents & Contracts trigger (`proposal_estimate_update`,
|
|
55
|
+
`masterType: "internal"`). The trigger union now accepts any `masterType` so
|
|
56
|
+
reads never throw, and `proposal_estimate_update` has typed support.
|
|
57
|
+
|
|
58
|
+
**Workflow settings preserved.** `update_workflow_actions` previously reset
|
|
59
|
+
`allowMultiple`, `stopOnResponse`, `autoMarkAsRead`,
|
|
60
|
+
`removeContactFromLastStep`, and `allowMultipleOpportunity` to defaults on every
|
|
61
|
+
save. It now preserves the workflow's current values, and exposes all five as
|
|
62
|
+
optional parameters.
|
|
63
|
+
|
|
64
|
+
**Contact search.** `search_contacts` now advances pages correctly (sends both
|
|
65
|
+
`startAfter` and `startAfterId` cursors), and uses the correct sort parameters:
|
|
66
|
+
`order` (asc/desc) instead of the rejected `sortOrder`, with `sortBy` limited to
|
|
67
|
+
`date_added`/`date_updated`.
|
|
68
|
+
|
|
3
69
|
## 3.28.0 — Tool allowlist (issue #1): cut context cost on big installs
|
|
4
70
|
|
|
5
71
|
Two new optional env vars let you gate which tools register at startup so
|
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.30.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",
|
|
@@ -682,7 +682,11 @@ var TriggerCommonSchema = import_zod3.z.object({
|
|
|
682
682
|
origin_id: import_zod3.z.string().optional(),
|
|
683
683
|
active: import_zod3.z.boolean().optional(),
|
|
684
684
|
workflow_id: import_zod3.z.string().optional(),
|
|
685
|
-
masterType
|
|
685
|
+
// Most native GHL triggers report masterType "highlevel", but some
|
|
686
|
+
// first-party features use other values (e.g. Documents & Contracts uses
|
|
687
|
+
// "internal"). Accept any string so reads never crash on the masterType
|
|
688
|
+
// discriminator and writes round-trip the original value unchanged.
|
|
689
|
+
masterType: import_zod3.z.string().optional(),
|
|
686
690
|
name: import_zod3.z.string().optional(),
|
|
687
691
|
actions: import_zod3.z.array(TriggerActionSchema).optional(),
|
|
688
692
|
schedule_config: import_zod3.z.record(import_zod3.z.unknown()).optional(),
|
|
@@ -987,6 +991,19 @@ var CustomObjectChangedTriggerSchema = TriggerCommonSchema.extend({
|
|
|
987
991
|
});
|
|
988
992
|
var ConvAiTriggerTriggerSchema = typedTrigger("conv_ai_trigger", [], "fieldless");
|
|
989
993
|
var ConvAiAutonomousTriggerTriggerSchema = typedTrigger("conv_ai_autonomous_trigger", [], "fieldless");
|
|
994
|
+
var ProposalEstimateUpdateTriggerSchema = TriggerCommonSchema.extend({
|
|
995
|
+
type: import_zod3.z.literal("proposal_estimate_update"),
|
|
996
|
+
conditions: import_zod3.z.array(import_zod3.z.object({
|
|
997
|
+
operator: import_zod3.z.string().optional(),
|
|
998
|
+
field: import_zod3.z.string().optional().describe(
|
|
999
|
+
"Documents & Contracts filter. Common fields: a document/estimate status path (values like sent, viewed, signed, completed, declined) and the document/template id. Exact field paths are not fully captured \u2014 pass through whatever the GHL UI shows."
|
|
1000
|
+
),
|
|
1001
|
+
value: import_zod3.z.unknown().optional(),
|
|
1002
|
+
title: import_zod3.z.string().optional(),
|
|
1003
|
+
type: import_zod3.z.string().optional(),
|
|
1004
|
+
id: import_zod3.z.string().optional()
|
|
1005
|
+
}).passthrough()).optional()
|
|
1006
|
+
});
|
|
990
1007
|
var UnknownTriggerSchema = TriggerCommonSchema.extend({
|
|
991
1008
|
type: import_zod3.z.string(),
|
|
992
1009
|
conditions: import_zod3.z.array(import_zod3.z.record(import_zod3.z.unknown())).optional()
|
|
@@ -1046,6 +1063,7 @@ var WorkflowTriggerSchema = import_zod3.z.union([
|
|
|
1046
1063
|
OrderSubmissionTriggerSchema,
|
|
1047
1064
|
ConvAiTriggerTriggerSchema,
|
|
1048
1065
|
ConvAiAutonomousTriggerTriggerSchema,
|
|
1066
|
+
ProposalEstimateUpdateTriggerSchema,
|
|
1049
1067
|
CustomObjectCreatedTriggerSchema,
|
|
1050
1068
|
CustomObjectChangedTriggerSchema,
|
|
1051
1069
|
FacebookLeadGenTriggerSchema,
|
|
@@ -1618,8 +1636,9 @@ ${errorBody}`
|
|
|
1618
1636
|
*/
|
|
1619
1637
|
async updateWorkflow(workflowId, updates) {
|
|
1620
1638
|
const current = await this.getWorkflow(workflowId);
|
|
1639
|
+
const effectiveStatus = updates.status ?? current.status;
|
|
1621
1640
|
if (updates.triggers !== void 0) {
|
|
1622
|
-
await this.syncTriggers(workflowId, current.triggers || [], updates.triggers);
|
|
1641
|
+
await this.syncTriggers(workflowId, current.triggers || [], updates.triggers, effectiveStatus);
|
|
1623
1642
|
}
|
|
1624
1643
|
const currentActions = current.workflowData?.templates || [];
|
|
1625
1644
|
const newActions = updates.actions ?? currentActions;
|
|
@@ -1631,15 +1650,18 @@ ${errorBody}`
|
|
|
1631
1650
|
const body = {
|
|
1632
1651
|
name: updates.name ?? current.name,
|
|
1633
1652
|
isRestoreRequest: true,
|
|
1634
|
-
status:
|
|
1653
|
+
status: effectiveStatus,
|
|
1635
1654
|
version: current.version,
|
|
1636
1655
|
dataVersion: current.dataVersion ?? 1,
|
|
1637
1656
|
timezone: current.timezone ?? "account",
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1657
|
+
// Preserve the workflow's existing settings unless explicitly overridden.
|
|
1658
|
+
// These were previously hardcoded, which silently reset re-enrollment
|
|
1659
|
+
// (allowMultiple) and other toggles on every action update.
|
|
1660
|
+
stopOnResponse: updates.stopOnResponse ?? current.stopOnResponse ?? false,
|
|
1661
|
+
allowMultiple: updates.allowMultiple ?? current.allowMultiple ?? false,
|
|
1662
|
+
allowMultipleOpportunity: updates.allowMultipleOpportunity ?? current.allowMultipleOpportunity ?? false,
|
|
1663
|
+
autoMarkAsRead: updates.autoMarkAsRead ?? current.autoMarkAsRead ?? false,
|
|
1664
|
+
removeContactFromLastStep: updates.removeContactFromLastStep ?? current.removeContactFromLastStep ?? true,
|
|
1643
1665
|
workflowData: { templates: linkedActions },
|
|
1644
1666
|
updatedBy: this.userId,
|
|
1645
1667
|
// Triggers live in Firestore and are managed out-of-band; the workflow
|
|
@@ -1661,16 +1683,30 @@ ${errorBody}`
|
|
|
1661
1683
|
return this.request("DELETE", `/${this.locationId}/${workflowId}`);
|
|
1662
1684
|
}
|
|
1663
1685
|
/**
|
|
1664
|
-
* Publish a draft workflow
|
|
1686
|
+
* Publish a draft workflow.
|
|
1687
|
+
*
|
|
1688
|
+
* Publishing must also re-activate the workflow's triggers. GHL stores a
|
|
1689
|
+
* trigger's `active` flag based on the write-time `status`, so triggers
|
|
1690
|
+
* created/edited while the workflow was a draft are written as "draft"
|
|
1691
|
+
* (active:false). Re-syncing them here under the now-"published" status
|
|
1692
|
+
* flips them live — otherwise a freshly published workflow's triggers never
|
|
1693
|
+
* fire (and the create → update_workflow_actions → publish_workflow flow
|
|
1694
|
+
* would leave new triggers inactive).
|
|
1665
1695
|
*/
|
|
1666
1696
|
async publishWorkflow(workflowId) {
|
|
1667
|
-
|
|
1697
|
+
const current = await this.getWorkflow(workflowId);
|
|
1698
|
+
return this.updateWorkflow(workflowId, {
|
|
1699
|
+
status: "published",
|
|
1700
|
+
triggers: current.triggers ?? []
|
|
1701
|
+
});
|
|
1668
1702
|
}
|
|
1669
1703
|
/**
|
|
1670
1704
|
* Create a workflow trigger. GHL generates the Firestore id and returns it.
|
|
1705
|
+
* `workflowStatus` is the owning workflow's published/draft state; it
|
|
1706
|
+
* determines whether the new trigger is written live or as a draft.
|
|
1671
1707
|
*/
|
|
1672
|
-
async createTrigger(workflowId, trigger) {
|
|
1673
|
-
const payload = this.buildTriggerPayload(workflowId, trigger);
|
|
1708
|
+
async createTrigger(workflowId, trigger, workflowStatus) {
|
|
1709
|
+
const payload = this.buildTriggerPayload(workflowId, trigger, workflowStatus);
|
|
1674
1710
|
const raw = await this.request("POST", `/${this.locationId}/trigger`, payload);
|
|
1675
1711
|
if (isRecord(raw) && typeof raw.id === "string") {
|
|
1676
1712
|
return raw.id;
|
|
@@ -1678,10 +1714,11 @@ ${errorBody}`
|
|
|
1678
1714
|
throw new Error(`Trigger creation did not return an id. Response: ${JSON.stringify(raw)}`);
|
|
1679
1715
|
}
|
|
1680
1716
|
/**
|
|
1681
|
-
* Update an existing workflow trigger.
|
|
1717
|
+
* Update an existing workflow trigger. `workflowStatus` is the owning
|
|
1718
|
+
* workflow's published/draft state (see buildTriggerPayload).
|
|
1682
1719
|
*/
|
|
1683
|
-
async updateTrigger(workflowId, trigger) {
|
|
1684
|
-
const payload = this.buildTriggerPayload(workflowId, trigger);
|
|
1720
|
+
async updateTrigger(workflowId, trigger, workflowStatus) {
|
|
1721
|
+
const payload = this.buildTriggerPayload(workflowId, trigger, workflowStatus);
|
|
1685
1722
|
return this.request("PUT", `/${this.locationId}/trigger/${trigger.id}`, payload);
|
|
1686
1723
|
}
|
|
1687
1724
|
/**
|
|
@@ -1695,7 +1732,7 @@ ${errorBody}`
|
|
|
1695
1732
|
* Triggers without an id are created; triggers present in current but
|
|
1696
1733
|
* missing from desired are deleted; triggers in both are updated in place.
|
|
1697
1734
|
*/
|
|
1698
|
-
async syncTriggers(workflowId, current, desired) {
|
|
1735
|
+
async syncTriggers(workflowId, current, desired, workflowStatus) {
|
|
1699
1736
|
const currentById = /* @__PURE__ */ new Map();
|
|
1700
1737
|
for (const t of current) {
|
|
1701
1738
|
if (typeof t.id === "string") currentById.set(t.id, t);
|
|
@@ -1711,9 +1748,9 @@ ${errorBody}`
|
|
|
1711
1748
|
}
|
|
1712
1749
|
for (const trigger of desired) {
|
|
1713
1750
|
if (hasTriggerId(trigger) && currentById.has(trigger.id)) {
|
|
1714
|
-
await this.updateTrigger(workflowId, trigger);
|
|
1751
|
+
await this.updateTrigger(workflowId, trigger, workflowStatus);
|
|
1715
1752
|
} else {
|
|
1716
|
-
await this.createTrigger(workflowId, trigger);
|
|
1753
|
+
await this.createTrigger(workflowId, trigger, workflowStatus);
|
|
1717
1754
|
}
|
|
1718
1755
|
}
|
|
1719
1756
|
}
|
|
@@ -1721,7 +1758,8 @@ ${errorBody}`
|
|
|
1721
1758
|
* Shape a trigger for the POST/PUT trigger endpoints, matching what the
|
|
1722
1759
|
* GHL UI sends. Missing fields are filled from the client's context.
|
|
1723
1760
|
*/
|
|
1724
|
-
buildTriggerPayload(workflowId, trigger) {
|
|
1761
|
+
buildTriggerPayload(workflowId, trigger, workflowStatus) {
|
|
1762
|
+
const isPublished = workflowStatus === "published";
|
|
1725
1763
|
return {
|
|
1726
1764
|
...trigger,
|
|
1727
1765
|
workflowId,
|
|
@@ -1731,8 +1769,8 @@ ${errorBody}`
|
|
|
1731
1769
|
belongs_to: "workflow",
|
|
1732
1770
|
actions: trigger.actions ?? [{ workflow_id: workflowId, type: "add_to_workflow" }],
|
|
1733
1771
|
schedule_config: trigger.schedule_config ?? {},
|
|
1734
|
-
active:
|
|
1735
|
-
status: "draft"
|
|
1772
|
+
active: isPublished,
|
|
1773
|
+
status: isPublished ? "published" : "draft"
|
|
1736
1774
|
};
|
|
1737
1775
|
}
|
|
1738
1776
|
/**
|
|
@@ -1958,20 +1996,25 @@ function registerContactTools(server2, client) {
|
|
|
1958
1996
|
locationId: import_zod6.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env."),
|
|
1959
1997
|
query: import_zod6.z.string().optional().describe("Search query to filter contacts (searches name, email, phone, etc.)."),
|
|
1960
1998
|
limit: import_zod6.z.number().optional().describe("Maximum number of contacts to return. Defaults to 20."),
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1999
|
+
startAfter: import_zod6.z.union([import_zod6.z.number(), import_zod6.z.string()]).optional().describe("Timestamp cursor (ms epoch) from the previous page's meta.startAfter. GHL cursor pagination needs BOTH startAfter and startAfterId to advance \u2014 pass both together."),
|
|
2000
|
+
startAfterId: import_zod6.z.string().optional().describe("Contact ID cursor from the previous page's meta.startAfterId. Use together with startAfter to advance pages."),
|
|
2001
|
+
sortBy: import_zod6.z.enum(["date_added", "date_updated"]).optional().describe("Field to sort by. GHL only accepts snake_case 'date_added' or 'date_updated'."),
|
|
2002
|
+
order: import_zod6.z.enum(["asc", "desc"]).optional().describe("Sort direction: 'asc' or 'desc'. (GHL rejects the legacy 'sortOrder' name \u2014 this maps to the 'order' query param.)")
|
|
1964
2003
|
},
|
|
1965
|
-
async ({ locationId: locationId2, query, limit, startAfterId, sortBy,
|
|
2004
|
+
async ({ locationId: locationId2, query, limit, startAfter, startAfterId, sortBy, order }) => {
|
|
1966
2005
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
1967
2006
|
const raw = await client.get("/contacts/", {
|
|
1968
2007
|
params: {
|
|
1969
2008
|
locationId: resolvedLocationId,
|
|
1970
2009
|
query,
|
|
1971
2010
|
limit: limit ?? 20,
|
|
2011
|
+
// Cursor pagination requires both the timestamp and the id; the GHL
|
|
2012
|
+
// nextPageUrl always carries both. Passing only startAfterId returns
|
|
2013
|
+
// the same page.
|
|
2014
|
+
startAfter,
|
|
1972
2015
|
startAfterId,
|
|
1973
2016
|
sortBy,
|
|
1974
|
-
|
|
2017
|
+
order
|
|
1975
2018
|
}
|
|
1976
2019
|
});
|
|
1977
2020
|
return ContactSearchResponseSchema.parse(raw);
|
|
@@ -4914,23 +4957,35 @@ function registerWebhookTools(server2, client) {
|
|
|
4914
4957
|
|
|
4915
4958
|
// src/tools/documents.ts
|
|
4916
4959
|
var import_zod32 = require("zod");
|
|
4960
|
+
var DOC_PAGE_MAX = 21;
|
|
4961
|
+
var DOC_SCAN_MAX_PAGES = 200;
|
|
4917
4962
|
function registerDocumentTools(server2, client) {
|
|
4918
4963
|
safeTool(
|
|
4919
4964
|
server2,
|
|
4920
4965
|
"list_documents",
|
|
4921
|
-
"List
|
|
4966
|
+
"List Documents & Contracts (sent/draft/completed agreements) in a GHL location. Filter by signature status and payment status. Backed by GET /proposals/document.",
|
|
4922
4967
|
{
|
|
4923
4968
|
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env."),
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4969
|
+
status: import_zod32.z.enum(["draft", "sent", "viewed", "completed", "declined", "expired", "voided"]).optional().describe("Filter by document signature status (e.g. 'sent', 'completed', 'draft'). Server-side filter."),
|
|
4970
|
+
paymentStatus: import_zod32.z.string().optional().describe("Filter by payment status (e.g. 'paid', 'pending', 'no_payment')."),
|
|
4971
|
+
query: import_zod32.z.string().optional().describe("Free-text search across document names/recipients."),
|
|
4972
|
+
dateFrom: import_zod32.z.string().optional().describe("Only documents updated on/after this date (ISO 8601)."),
|
|
4973
|
+
dateTo: import_zod32.z.string().optional().describe("Only documents updated on/before this date (ISO 8601)."),
|
|
4974
|
+
limit: import_zod32.z.number().optional().describe("Max documents to return. Defaults to 20. GHL caps this at 21 per page."),
|
|
4975
|
+
skip: import_zod32.z.number().optional().describe("Number of records to skip for pagination.")
|
|
4976
|
+
},
|
|
4977
|
+
async ({ locationId: locationId2, status, paymentStatus, query, dateFrom, dateTo, limit, skip }) => {
|
|
4928
4978
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
4929
|
-
return client.get(`/
|
|
4979
|
+
return client.get(`/proposals/document`, {
|
|
4930
4980
|
params: {
|
|
4931
4981
|
locationId: resolvedLocationId,
|
|
4932
|
-
|
|
4933
|
-
|
|
4982
|
+
status,
|
|
4983
|
+
paymentStatus,
|
|
4984
|
+
query,
|
|
4985
|
+
dateFrom,
|
|
4986
|
+
dateTo,
|
|
4987
|
+
limit: Math.min(limit ?? 20, DOC_PAGE_MAX),
|
|
4988
|
+
skip
|
|
4934
4989
|
}
|
|
4935
4990
|
});
|
|
4936
4991
|
}
|
|
@@ -4938,47 +4993,147 @@ function registerDocumentTools(server2, client) {
|
|
|
4938
4993
|
safeTool(
|
|
4939
4994
|
server2,
|
|
4940
4995
|
"get_document",
|
|
4941
|
-
"Retrieve a single
|
|
4996
|
+
"Retrieve a single Document & Contract by its ID, including signature/payment status and recipients. NOTE: GHL's public API has no get-by-id route, so this scans the documents list (GET /proposals/document) to find a match.",
|
|
4942
4997
|
{
|
|
4943
|
-
documentId: import_zod32.z.string().describe("The document ID to retrieve."),
|
|
4998
|
+
documentId: import_zod32.z.string().describe("The document ID to retrieve (the document's _id)."),
|
|
4944
4999
|
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
|
|
4945
5000
|
},
|
|
4946
5001
|
async ({ documentId, locationId: locationId2 }) => {
|
|
4947
5002
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
5003
|
+
let skip = 0;
|
|
5004
|
+
let total = Infinity;
|
|
5005
|
+
let scannedAll = true;
|
|
5006
|
+
for (let page = 0; page < DOC_SCAN_MAX_PAGES && skip < total; page++) {
|
|
5007
|
+
const res = await client.get(`/proposals/document`, {
|
|
5008
|
+
params: { locationId: resolvedLocationId, limit: DOC_PAGE_MAX, skip }
|
|
5009
|
+
});
|
|
5010
|
+
const docs = res?.documents ?? [];
|
|
5011
|
+
if (typeof res?.total === "number") total = res.total;
|
|
5012
|
+
const match = docs.find(
|
|
5013
|
+
(d) => d._id === documentId || d.documentId === documentId || d.id === documentId
|
|
5014
|
+
);
|
|
5015
|
+
if (match) return match;
|
|
5016
|
+
if (docs.length < DOC_PAGE_MAX) break;
|
|
5017
|
+
skip += DOC_PAGE_MAX;
|
|
5018
|
+
if (page === DOC_SCAN_MAX_PAGES - 1 && skip < total) scannedAll = false;
|
|
5019
|
+
}
|
|
5020
|
+
return {
|
|
5021
|
+
found: false,
|
|
5022
|
+
documentId,
|
|
5023
|
+
message: scannedAll ? `No document with id ${documentId} found in location ${resolvedLocationId}. Use list_documents to browse available documents.` : `No document with id ${documentId} found within the first ${DOC_SCAN_MAX_PAGES * DOC_PAGE_MAX} documents scanned. This location has more; narrow the set with list_documents filters (status, query, date range).`
|
|
5024
|
+
};
|
|
5025
|
+
}
|
|
5026
|
+
);
|
|
5027
|
+
safeTool(
|
|
5028
|
+
server2,
|
|
5029
|
+
"send_document",
|
|
5030
|
+
"Send/dispatch an EXISTING Document & Contract to the recipients already on it. To create and send a new contract to a contact, use send_document_template instead. Backed by POST /proposals/document/send.",
|
|
5031
|
+
{
|
|
5032
|
+
documentId: import_zod32.z.string().describe("The document ID to send."),
|
|
5033
|
+
sentBy: import_zod32.z.string().optional().describe("GHL user ID of the sender (required by the API; falls back to GHL_USER_ID env)."),
|
|
5034
|
+
documentName: import_zod32.z.string().optional().describe("Override the document name shown to recipients."),
|
|
5035
|
+
medium: import_zod32.z.string().optional().describe("Delivery medium, e.g. 'email'. Defaults to email."),
|
|
5036
|
+
ccRecipients: import_zod32.z.array(import_zod32.z.record(import_zod32.z.unknown())).optional().describe("Optional CC recipients: [{id, email, contactName, firstName, lastName, imageUrl}]."),
|
|
5037
|
+
notificationSettings: import_zod32.z.record(import_zod32.z.unknown()).optional().describe("Optional sender/receiver notification settings: {sender:{fromName,fromEmail}, receive:{subject,templateId}}."),
|
|
5038
|
+
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
|
|
5039
|
+
},
|
|
5040
|
+
async ({ documentId, sentBy, documentName, medium, ccRecipients, notificationSettings, locationId: locationId2 }) => {
|
|
5041
|
+
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
5042
|
+
const resolvedSentBy = sentBy ?? process.env.GHL_USER_ID;
|
|
5043
|
+
if (!resolvedSentBy) {
|
|
5044
|
+
throw new Error(
|
|
5045
|
+
"sentBy (sender user ID) is required. Provide it as a parameter or set GHL_USER_ID in your env."
|
|
5046
|
+
);
|
|
5047
|
+
}
|
|
5048
|
+
const body = {
|
|
5049
|
+
locationId: resolvedLocationId,
|
|
5050
|
+
documentId,
|
|
5051
|
+
sentBy: resolvedSentBy
|
|
5052
|
+
};
|
|
5053
|
+
if (documentName !== void 0) body.documentName = documentName;
|
|
5054
|
+
if (medium !== void 0) body.medium = medium;
|
|
5055
|
+
if (ccRecipients !== void 0) body.ccRecipients = ccRecipients;
|
|
5056
|
+
if (notificationSettings !== void 0) body.notificationSettings = notificationSettings;
|
|
5057
|
+
return client.post(`/proposals/document/send`, { body });
|
|
4951
5058
|
}
|
|
4952
5059
|
);
|
|
4953
5060
|
safeTool(
|
|
4954
5061
|
server2,
|
|
4955
5062
|
"delete_document",
|
|
4956
|
-
"Delete a
|
|
5063
|
+
"Delete or void a Document & Contract. NOTE: GHL's public API does NOT support deleting or voiding documents \u2014 this must be done in the GHL UI (Payments \u2192 Documents & Contracts \u2192 the document's \u22EF menu \u2192 Delete/Void). This tool reports that limitation rather than failing silently.",
|
|
4957
5064
|
{
|
|
4958
|
-
documentId: import_zod32.z.string().describe("The document ID to delete."),
|
|
5065
|
+
documentId: import_zod32.z.string().describe("The document ID you intend to delete/void."),
|
|
4959
5066
|
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
|
|
4960
5067
|
},
|
|
4961
|
-
async ({ documentId
|
|
5068
|
+
async ({ documentId }) => {
|
|
5069
|
+
return {
|
|
5070
|
+
supported: false,
|
|
5071
|
+
documentId,
|
|
5072
|
+
message: "GHL's public Documents & Contracts API does not expose a delete or void/recall endpoint. Delete or void this document in the GHL UI: Payments \u2192 Documents & Contracts \u2192 open the document \u2192 \u22EF menu \u2192 Delete or Void."
|
|
5073
|
+
};
|
|
5074
|
+
}
|
|
5075
|
+
);
|
|
5076
|
+
safeTool(
|
|
5077
|
+
server2,
|
|
5078
|
+
"list_document_templates",
|
|
5079
|
+
"List Documents & Contracts TEMPLATES (the reusable contract templates you build, e.g. agreements you send to clients). Backed by GET /proposals/templates.",
|
|
5080
|
+
{
|
|
5081
|
+
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env."),
|
|
5082
|
+
type: import_zod32.z.string().optional().describe("Filter by template type (e.g. 'proposal')."),
|
|
5083
|
+
name: import_zod32.z.string().optional().describe("Filter by template name (partial match)."),
|
|
5084
|
+
isPublicDocument: import_zod32.z.boolean().optional().describe("Filter for public templates only."),
|
|
5085
|
+
userId: import_zod32.z.string().optional().describe("Filter by the user who created the template."),
|
|
5086
|
+
dateFrom: import_zod32.z.string().optional().describe("Only templates updated on/after this date (ISO 8601)."),
|
|
5087
|
+
dateTo: import_zod32.z.string().optional().describe("Only templates updated on/before this date (ISO 8601)."),
|
|
5088
|
+
limit: import_zod32.z.number().optional().describe("Max templates to return. Defaults to 20."),
|
|
5089
|
+
skip: import_zod32.z.number().optional().describe("Number of records to skip for pagination.")
|
|
5090
|
+
},
|
|
5091
|
+
async ({ locationId: locationId2, type, name, isPublicDocument, userId, dateFrom, dateTo, limit, skip }) => {
|
|
4962
5092
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
4963
|
-
return client.
|
|
4964
|
-
params: {
|
|
5093
|
+
return client.get(`/proposals/templates`, {
|
|
5094
|
+
params: {
|
|
5095
|
+
locationId: resolvedLocationId,
|
|
5096
|
+
type,
|
|
5097
|
+
name,
|
|
5098
|
+
isPublicDocument,
|
|
5099
|
+
userId,
|
|
5100
|
+
dateFrom,
|
|
5101
|
+
dateTo,
|
|
5102
|
+
limit: limit ?? 20,
|
|
5103
|
+
skip
|
|
5104
|
+
}
|
|
4965
5105
|
});
|
|
4966
5106
|
}
|
|
4967
5107
|
);
|
|
4968
5108
|
safeTool(
|
|
4969
5109
|
server2,
|
|
4970
|
-
"
|
|
4971
|
-
"
|
|
5110
|
+
"send_document_template",
|
|
5111
|
+
"Create and send a new Document & Contract to a contact from a template. This is how you send a contract to someone. Backed by POST /proposals/templates/send.",
|
|
4972
5112
|
{
|
|
4973
|
-
|
|
4974
|
-
contactId: import_zod32.z.string().describe("The contact ID to send the
|
|
5113
|
+
templateId: import_zod32.z.string().describe("The template ID to send (from list_document_templates)."),
|
|
5114
|
+
contactId: import_zod32.z.string().describe("The contact ID to send the contract to."),
|
|
5115
|
+
userId: import_zod32.z.string().optional().describe("GHL user ID of the sender (required by the API; falls back to GHL_USER_ID env)."),
|
|
5116
|
+
sendDocument: import_zod32.z.boolean().optional().describe("If true (default), actually dispatch the document. If false, only create the draft."),
|
|
5117
|
+
opportunityId: import_zod32.z.string().optional().describe("Optionally link the document to an opportunity."),
|
|
4975
5118
|
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
|
|
4976
5119
|
},
|
|
4977
|
-
async ({
|
|
5120
|
+
async ({ templateId, contactId, userId, sendDocument, opportunityId, locationId: locationId2 }) => {
|
|
4978
5121
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
5122
|
+
const resolvedUserId = userId ?? process.env.GHL_USER_ID;
|
|
5123
|
+
if (!resolvedUserId) {
|
|
5124
|
+
throw new Error(
|
|
5125
|
+
"userId (sender user ID) is required. Provide it as a parameter or set GHL_USER_ID in your env."
|
|
5126
|
+
);
|
|
5127
|
+
}
|
|
5128
|
+
const body = {
|
|
5129
|
+
templateId,
|
|
5130
|
+
contactId,
|
|
5131
|
+
userId: resolvedUserId,
|
|
5132
|
+
locationId: resolvedLocationId
|
|
5133
|
+
};
|
|
5134
|
+
if (sendDocument !== void 0) body.sendDocument = sendDocument;
|
|
5135
|
+
if (opportunityId !== void 0) body.opportunityId = opportunityId;
|
|
5136
|
+
return client.post(`/proposals/templates/send`, { body });
|
|
4982
5137
|
}
|
|
4983
5138
|
);
|
|
4984
5139
|
}
|
|
@@ -5135,15 +5290,25 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
5135
5290
|
name: import_zod33.z.string().optional().describe("New workflow name."),
|
|
5136
5291
|
status: import_zod33.z.string().optional().describe("New status: 'draft' or 'published'."),
|
|
5137
5292
|
actions: import_zod33.z.array(ActionSchema).optional().describe("Array of workflow actions/steps. For linear flows, provide in order \u2014 chaining is automatic."),
|
|
5138
|
-
triggers: import_zod33.z.array(WorkflowTriggerSchema).optional().describe("Array of workflow triggers.")
|
|
5139
|
-
|
|
5140
|
-
|
|
5293
|
+
triggers: import_zod33.z.array(WorkflowTriggerSchema).optional().describe("Array of workflow triggers."),
|
|
5294
|
+
allowMultiple: import_zod33.z.boolean().optional().describe("Allow a contact to be enrolled more than once (re-enrollment). If omitted, the workflow's current value is preserved."),
|
|
5295
|
+
stopOnResponse: import_zod33.z.boolean().optional().describe("Stop the workflow when the contact replies. If omitted, the current value is preserved."),
|
|
5296
|
+
autoMarkAsRead: import_zod33.z.boolean().optional().describe("Auto-mark conversations as read. If omitted, the current value is preserved."),
|
|
5297
|
+
removeContactFromLastStep: import_zod33.z.boolean().optional().describe("Remove the contact when they reach the last step. If omitted, the current value is preserved."),
|
|
5298
|
+
allowMultipleOpportunity: import_zod33.z.boolean().optional().describe("Allow creating multiple opportunities. If omitted, the current value is preserved.")
|
|
5299
|
+
},
|
|
5300
|
+
async ({ workflowId, name, status, actions, triggers, allowMultiple, stopOnResponse, autoMarkAsRead, removeContactFromLastStep, allowMultipleOpportunity }) => {
|
|
5141
5301
|
try {
|
|
5142
5302
|
const result = await client.updateWorkflow(workflowId, {
|
|
5143
5303
|
name,
|
|
5144
5304
|
status,
|
|
5145
5305
|
actions,
|
|
5146
|
-
triggers
|
|
5306
|
+
triggers,
|
|
5307
|
+
allowMultiple,
|
|
5308
|
+
stopOnResponse,
|
|
5309
|
+
autoMarkAsRead,
|
|
5310
|
+
removeContactFromLastStep,
|
|
5311
|
+
allowMultipleOpportunity
|
|
5147
5312
|
});
|
|
5148
5313
|
return jsonResponse(result);
|
|
5149
5314
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.30.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",
|