@elitedcs/ghl-mcp 3.27.2 → 3.29.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 +77 -0
- package/README.md +39 -0
- package/dist/index.js +323 -58
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.29.0 — Documents & Contracts API fix + workflow/contact bug fixes
|
|
4
|
+
|
|
5
|
+
Five confirmed bugs fixed, verified live against the GHL API.
|
|
6
|
+
|
|
7
|
+
**Documents & Contracts (the big one).** The document tools were hitting a
|
|
8
|
+
non-existent `/documents/` path and returning 404. GHL's real Documents &
|
|
9
|
+
Contracts API lives under `/proposals/*`:
|
|
10
|
+
|
|
11
|
+
- `list_documents` now calls `GET /proposals/document` with real filters:
|
|
12
|
+
`status` (draft/sent/viewed/completed/declined), `paymentStatus`, `query`,
|
|
13
|
+
`dateFrom`/`dateTo`, and pagination (`limit` capped at GHL's max of 21, `skip`).
|
|
14
|
+
- `get_document` resolves a document by id by scanning the list (GHL exposes no
|
|
15
|
+
public get-by-id route).
|
|
16
|
+
- `send_document` now calls `POST /proposals/document/send` to dispatch an
|
|
17
|
+
existing document to its recipients.
|
|
18
|
+
- `delete_document` reports honestly that GHL's public API has no delete/void
|
|
19
|
+
route (do it in the UI) instead of failing on a dead path.
|
|
20
|
+
- **New:** `list_document_templates` (`GET /proposals/templates`) lists your
|
|
21
|
+
reusable contract templates.
|
|
22
|
+
- **New:** `send_document_template` (`POST /proposals/templates/send`) creates
|
|
23
|
+
and sends a contract to a contact from a template.
|
|
24
|
+
|
|
25
|
+
**Workflow triggers.** `get_workflow_full` and `update_workflow_actions` no
|
|
26
|
+
longer crash on the Documents & Contracts trigger (`proposal_estimate_update`,
|
|
27
|
+
`masterType: "internal"`). The trigger union now accepts any `masterType` so
|
|
28
|
+
reads never throw, and `proposal_estimate_update` has typed support.
|
|
29
|
+
|
|
30
|
+
**Workflow settings preserved.** `update_workflow_actions` previously reset
|
|
31
|
+
`allowMultiple`, `stopOnResponse`, `autoMarkAsRead`,
|
|
32
|
+
`removeContactFromLastStep`, and `allowMultipleOpportunity` to defaults on every
|
|
33
|
+
save. It now preserves the workflow's current values, and exposes all five as
|
|
34
|
+
optional parameters.
|
|
35
|
+
|
|
36
|
+
**Contact search.** `search_contacts` now advances pages correctly (sends both
|
|
37
|
+
`startAfter` and `startAfterId` cursors), and uses the correct sort parameters:
|
|
38
|
+
`order` (asc/desc) instead of the rejected `sortOrder`, with `sortBy` limited to
|
|
39
|
+
`date_added`/`date_updated`.
|
|
40
|
+
|
|
41
|
+
## 3.28.0 — Tool allowlist (issue #1): cut context cost on big installs
|
|
42
|
+
|
|
43
|
+
Two new optional env vars let you gate which tools register at startup so
|
|
44
|
+
unused schemas never load into context. Closes [issue #1](https://github.com/drjerryrelth/ghl-command-feedback/issues/1).
|
|
45
|
+
|
|
46
|
+
**New env vars (both optional, both default-empty):**
|
|
47
|
+
|
|
48
|
+
- `GHL_ENABLED_MODULES` — comma-separated module names to register
|
|
49
|
+
(e.g. `contacts,conversations,locations,custom-objects`).
|
|
50
|
+
- `GHL_ENABLED_TOOLS` — comma-separated tool names to register
|
|
51
|
+
(e.g. `search_contacts,get_contact,update_custom_value`).
|
|
52
|
+
|
|
53
|
+
**Filter precedence:**
|
|
54
|
+
|
|
55
|
+
- Neither set → every tool registers (backward compatible).
|
|
56
|
+
- Modules only → tools in those modules register.
|
|
57
|
+
- Tools only → those exact tool names register.
|
|
58
|
+
- Both set → UNION (a tool registers if its module is enabled OR its
|
|
59
|
+
name is explicitly listed).
|
|
60
|
+
|
|
61
|
+
**Always-on tools** never get filtered out, so setup and recovery still
|
|
62
|
+
work even with a heavily-restricted allowlist:
|
|
63
|
+
`setup_ghl_mcp`, `request_license`, `get_mcp_version`,
|
|
64
|
+
`auto_capture_firebase_script`, `enable_workflow_builder`, `health_check`.
|
|
65
|
+
|
|
66
|
+
**Validation + logging:**
|
|
67
|
+
|
|
68
|
+
- Whitespace and commas both work as separators. Matching is case-insensitive.
|
|
69
|
+
- Unrecognized names log a one-line WARNING to stderr and are ignored
|
|
70
|
+
(startup never aborts).
|
|
71
|
+
- When the allowlist is active, startup logs one summary:
|
|
72
|
+
`Tool allowlist active: registered N of M tools (modules=[...]; explicit-tools=[...])`.
|
|
73
|
+
- When unset, zero extra noise — existing logs are unchanged.
|
|
74
|
+
|
|
75
|
+
**Motivation (from the issue):** running 25+ registered locations with
|
|
76
|
+
all 212 tools registered burned tokens on every message in chats that
|
|
77
|
+
didn't need GHL. Now buyers can restrict the surface to the modules
|
|
78
|
+
they actually use.
|
|
79
|
+
|
|
3
80
|
## 3.27.2 — Fix license metadata: MIT → Proprietary
|
|
4
81
|
|
|
5
82
|
Metadata-only release. Corrects the npm package's stated license, which
|
package/README.md
CHANGED
|
@@ -709,9 +709,48 @@ Source repo is private. Contributors need an invitation from `drjerryrelth`. The
|
|
|
709
709
|
| `GHL_USER_ID` | No* | Your GHL user ID. Required for workflow builder tools. |
|
|
710
710
|
| `GHL_FIREBASE_API_KEY` | No* | Firebase API key from GHL's auth system. Required for workflow builder tools. |
|
|
711
711
|
| `GHL_FIREBASE_REFRESH_TOKEN` | No* | Firebase refresh token. Required for workflow builder tools. May rotate periodically. |
|
|
712
|
+
| `GHL_ENABLED_MODULES` | No | Comma-separated module names to limit which tool groups register at startup (e.g. `contacts,conversations,locations`). Unset = every module registers (default). See "Reducing context / token usage" below. |
|
|
713
|
+
| `GHL_ENABLED_TOOLS` | No | Comma-separated individual tool names to register (e.g. `search_contacts,get_contact,update_custom_value`). Set alongside `GHL_ENABLED_MODULES` to add specific tools on top of whole-module enables. |
|
|
712
714
|
|
|
713
715
|
*Required only for the workflow builder (internal API) tools. The standard API tools work without these.
|
|
714
716
|
|
|
717
|
+
### Reducing context / token usage
|
|
718
|
+
|
|
719
|
+
Every registered MCP tool's schema is shipped to the model on every message. With 212 tools that's a meaningful per-message context cost even in chats that never touch GHL. If you only use a slice of GHL Command, restrict the tool surface with `GHL_ENABLED_MODULES` and/or `GHL_ENABLED_TOOLS`:
|
|
720
|
+
|
|
721
|
+
```jsonc
|
|
722
|
+
// Claude Desktop config — enable whole modules
|
|
723
|
+
"env": {
|
|
724
|
+
"GHL_API_KEY": "pit-...",
|
|
725
|
+
"GHL_LOCATION_ID": "...",
|
|
726
|
+
"GHL_ENABLED_MODULES": "contacts,conversations,locations,custom-objects"
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
```jsonc
|
|
731
|
+
// Or pin to individual tools
|
|
732
|
+
"env": {
|
|
733
|
+
"GHL_API_KEY": "pit-...",
|
|
734
|
+
"GHL_LOCATION_ID": "...",
|
|
735
|
+
"GHL_ENABLED_TOOLS": "search_contacts,get_contact,update_custom_value"
|
|
736
|
+
}
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Rules of the road:
|
|
740
|
+
|
|
741
|
+
- **Neither set → every tool registers** (default, backward compatible — your existing config keeps working unchanged).
|
|
742
|
+
- **`GHL_ENABLED_MODULES` only** → register tools in those modules.
|
|
743
|
+
- **`GHL_ENABLED_TOOLS` only** → register only those exact tool names.
|
|
744
|
+
- **Both set** → register the **union**: a tool registers if its module is in `GHL_ENABLED_MODULES` OR its name is in `GHL_ENABLED_TOOLS`. (Whole modules plus a few extra tools.)
|
|
745
|
+
- Whitespace and commas both work as separators. Matching is case-insensitive.
|
|
746
|
+
- Always-on tools register regardless of the allowlist so setup and recovery still work: `setup_ghl_mcp`, `request_license`, `get_mcp_version`, `auto_capture_firebase_script`, `enable_workflow_builder`, `health_check`.
|
|
747
|
+
- Unrecognized module/tool names log a one-line WARNING to stderr and are ignored. Startup never aborts.
|
|
748
|
+
- On startup with the allowlist active, the server logs one summary line: `Tool allowlist active: registered N of M tools (...)`.
|
|
749
|
+
|
|
750
|
+
**Valid module names** (use these in `GHL_ENABLED_MODULES`):
|
|
751
|
+
|
|
752
|
+
`contacts`, `conversations`, `opportunities`, `calendars`, `locations`, `workflows`, `funnels`, `forms`, `surveys`, `payments`, `products`, `invoices`, `campaigns`, `users`, `media`, `social-planner`, `courses`, `businesses`, `blogs`, `emails`, `trigger-links`, `custom-objects`, `associations`, `estimates`, `coupons`, `webhooks`, `documents`, `bulk-operations`, `account-export`, `template-deployer`, `workflow-builder`, `funnel-builder`, `form-builder`, `pipeline-builder`, `workflow-cloner`, `smart-lists`, `reputation`, `email-campaigns`, `email-builder`, `memberships`, `validators`, `diagnostics`, `location-switcher`
|
|
753
|
+
|
|
715
754
|
---
|
|
716
755
|
|
|
717
756
|
## Troubleshooting
|
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.29.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,
|
|
@@ -1635,11 +1653,14 @@ ${errorBody}`
|
|
|
1635
1653
|
version: current.version,
|
|
1636
1654
|
dataVersion: current.dataVersion ?? 1,
|
|
1637
1655
|
timezone: current.timezone ?? "account",
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1656
|
+
// Preserve the workflow's existing settings unless explicitly overridden.
|
|
1657
|
+
// These were previously hardcoded, which silently reset re-enrollment
|
|
1658
|
+
// (allowMultiple) and other toggles on every action update.
|
|
1659
|
+
stopOnResponse: updates.stopOnResponse ?? current.stopOnResponse ?? false,
|
|
1660
|
+
allowMultiple: updates.allowMultiple ?? current.allowMultiple ?? false,
|
|
1661
|
+
allowMultipleOpportunity: updates.allowMultipleOpportunity ?? current.allowMultipleOpportunity ?? false,
|
|
1662
|
+
autoMarkAsRead: updates.autoMarkAsRead ?? current.autoMarkAsRead ?? false,
|
|
1663
|
+
removeContactFromLastStep: updates.removeContactFromLastStep ?? current.removeContactFromLastStep ?? true,
|
|
1643
1664
|
workflowData: { templates: linkedActions },
|
|
1644
1665
|
updatedBy: this.userId,
|
|
1645
1666
|
// Triggers live in Firestore and are managed out-of-band; the workflow
|
|
@@ -1776,6 +1797,89 @@ ${errorBody}`
|
|
|
1776
1797
|
}
|
|
1777
1798
|
};
|
|
1778
1799
|
|
|
1800
|
+
// src/tools/tool-filter.ts
|
|
1801
|
+
var ALWAYS_ON_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1802
|
+
"setup_ghl_mcp",
|
|
1803
|
+
"request_license",
|
|
1804
|
+
"get_mcp_version",
|
|
1805
|
+
"auto_capture_firebase_script",
|
|
1806
|
+
"enable_workflow_builder",
|
|
1807
|
+
"health_check"
|
|
1808
|
+
]);
|
|
1809
|
+
function parseList(raw) {
|
|
1810
|
+
if (raw === void 0) return null;
|
|
1811
|
+
const items = raw.split(/[,\s]+/).map((x) => x.trim().toLowerCase()).filter((x) => x.length > 0);
|
|
1812
|
+
return items.length > 0 ? items : null;
|
|
1813
|
+
}
|
|
1814
|
+
function parseAllowlist(env) {
|
|
1815
|
+
const moduleList = parseList(env.GHL_ENABLED_MODULES);
|
|
1816
|
+
const toolList = parseList(env.GHL_ENABLED_TOOLS);
|
|
1817
|
+
return {
|
|
1818
|
+
enabledModules: moduleList ? new Set(moduleList) : null,
|
|
1819
|
+
enabledTools: toolList ? new Set(toolList) : null,
|
|
1820
|
+
rawModuleInput: moduleList,
|
|
1821
|
+
rawToolInput: toolList
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
function isAllowlistActive(config3) {
|
|
1825
|
+
return config3.enabledModules !== null || config3.enabledTools !== null;
|
|
1826
|
+
}
|
|
1827
|
+
function shouldRegister(toolName, moduleName, config3) {
|
|
1828
|
+
if (ALWAYS_ON_TOOL_NAMES.has(toolName)) return true;
|
|
1829
|
+
if (!isAllowlistActive(config3)) return true;
|
|
1830
|
+
const toolLower = toolName.toLowerCase();
|
|
1831
|
+
const moduleLower = moduleName.toLowerCase();
|
|
1832
|
+
const moduleEnabled = config3.enabledModules?.has(moduleLower) ?? false;
|
|
1833
|
+
const toolEnabled = config3.enabledTools?.has(toolLower) ?? false;
|
|
1834
|
+
return moduleEnabled || toolEnabled;
|
|
1835
|
+
}
|
|
1836
|
+
function wrapServerForModule(realServer, moduleName, config3, attemptedTools, registeredTools) {
|
|
1837
|
+
return new Proxy(realServer, {
|
|
1838
|
+
get(target, prop, receiver) {
|
|
1839
|
+
if (prop === "tool") {
|
|
1840
|
+
return (name, ...rest) => {
|
|
1841
|
+
attemptedTools.add(name);
|
|
1842
|
+
if (!shouldRegister(name, moduleName, config3)) {
|
|
1843
|
+
return void 0;
|
|
1844
|
+
}
|
|
1845
|
+
registeredTools.add(name);
|
|
1846
|
+
return target.tool(name, ...rest);
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
return Reflect.get(target, prop, receiver);
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
function validateAndLog(config3, knownModules, attemptedTools, registeredTools, log = (m) => process.stderr.write(m)) {
|
|
1854
|
+
if (!isAllowlistActive(config3)) return;
|
|
1855
|
+
if (config3.rawModuleInput) {
|
|
1856
|
+
const knownLower = new Set([...knownModules].map((m) => m.toLowerCase()));
|
|
1857
|
+
const unknown = config3.rawModuleInput.filter((m) => !knownLower.has(m));
|
|
1858
|
+
if (unknown.length > 0) {
|
|
1859
|
+
log(
|
|
1860
|
+
`[ghl-mcp] WARNING: GHL_ENABLED_MODULES has unrecognized name(s): ${unknown.join(", ")}. Known modules: ${[...knownModules].sort().join(", ")}.
|
|
1861
|
+
`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (config3.rawToolInput) {
|
|
1866
|
+
const attemptedLower = new Set([...attemptedTools].map((t) => t.toLowerCase()));
|
|
1867
|
+
const unknown = config3.rawToolInput.filter((t) => !attemptedLower.has(t));
|
|
1868
|
+
if (unknown.length > 0) {
|
|
1869
|
+
log(
|
|
1870
|
+
`[ghl-mcp] WARNING: GHL_ENABLED_TOOLS has unrecognized name(s): ${unknown.join(", ")}.
|
|
1871
|
+
`
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
const moduleSummary = config3.enabledModules && config3.enabledModules.size > 0 ? `modules=[${[...config3.enabledModules].sort().join(",")}]` : "modules=(none)";
|
|
1876
|
+
const toolSummary = config3.enabledTools && config3.enabledTools.size > 0 ? `explicit-tools=[${[...config3.enabledTools].sort().join(",")}]` : "explicit-tools=(none)";
|
|
1877
|
+
log(
|
|
1878
|
+
`[ghl-mcp] Tool allowlist active: registered ${registeredTools.size} of ${attemptedTools.size} tools (${moduleSummary}; ${toolSummary}).
|
|
1879
|
+
`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1779
1883
|
// src/tools/contacts.ts
|
|
1780
1884
|
var import_zod6 = require("zod");
|
|
1781
1885
|
|
|
@@ -1875,20 +1979,25 @@ function registerContactTools(server2, client) {
|
|
|
1875
1979
|
locationId: import_zod6.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env."),
|
|
1876
1980
|
query: import_zod6.z.string().optional().describe("Search query to filter contacts (searches name, email, phone, etc.)."),
|
|
1877
1981
|
limit: import_zod6.z.number().optional().describe("Maximum number of contacts to return. Defaults to 20."),
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1982
|
+
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."),
|
|
1983
|
+
startAfterId: import_zod6.z.string().optional().describe("Contact ID cursor from the previous page's meta.startAfterId. Use together with startAfter to advance pages."),
|
|
1984
|
+
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'."),
|
|
1985
|
+
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.)")
|
|
1881
1986
|
},
|
|
1882
|
-
async ({ locationId: locationId2, query, limit, startAfterId, sortBy,
|
|
1987
|
+
async ({ locationId: locationId2, query, limit, startAfter, startAfterId, sortBy, order }) => {
|
|
1883
1988
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
1884
1989
|
const raw = await client.get("/contacts/", {
|
|
1885
1990
|
params: {
|
|
1886
1991
|
locationId: resolvedLocationId,
|
|
1887
1992
|
query,
|
|
1888
1993
|
limit: limit ?? 20,
|
|
1994
|
+
// Cursor pagination requires both the timestamp and the id; the GHL
|
|
1995
|
+
// nextPageUrl always carries both. Passing only startAfterId returns
|
|
1996
|
+
// the same page.
|
|
1997
|
+
startAfter,
|
|
1889
1998
|
startAfterId,
|
|
1890
1999
|
sortBy,
|
|
1891
|
-
|
|
2000
|
+
order
|
|
1892
2001
|
}
|
|
1893
2002
|
});
|
|
1894
2003
|
return ContactSearchResponseSchema.parse(raw);
|
|
@@ -4831,23 +4940,35 @@ function registerWebhookTools(server2, client) {
|
|
|
4831
4940
|
|
|
4832
4941
|
// src/tools/documents.ts
|
|
4833
4942
|
var import_zod32 = require("zod");
|
|
4943
|
+
var DOC_PAGE_MAX = 21;
|
|
4944
|
+
var DOC_SCAN_MAX_PAGES = 200;
|
|
4834
4945
|
function registerDocumentTools(server2, client) {
|
|
4835
4946
|
safeTool(
|
|
4836
4947
|
server2,
|
|
4837
4948
|
"list_documents",
|
|
4838
|
-
"List
|
|
4949
|
+
"List Documents & Contracts (sent/draft/completed agreements) in a GHL location. Filter by signature status and payment status. Backed by GET /proposals/document.",
|
|
4839
4950
|
{
|
|
4840
4951
|
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env."),
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4952
|
+
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."),
|
|
4953
|
+
paymentStatus: import_zod32.z.string().optional().describe("Filter by payment status (e.g. 'paid', 'pending', 'no_payment')."),
|
|
4954
|
+
query: import_zod32.z.string().optional().describe("Free-text search across document names/recipients."),
|
|
4955
|
+
dateFrom: import_zod32.z.string().optional().describe("Only documents updated on/after this date (ISO 8601)."),
|
|
4956
|
+
dateTo: import_zod32.z.string().optional().describe("Only documents updated on/before this date (ISO 8601)."),
|
|
4957
|
+
limit: import_zod32.z.number().optional().describe("Max documents to return. Defaults to 20. GHL caps this at 21 per page."),
|
|
4958
|
+
skip: import_zod32.z.number().optional().describe("Number of records to skip for pagination.")
|
|
4959
|
+
},
|
|
4960
|
+
async ({ locationId: locationId2, status, paymentStatus, query, dateFrom, dateTo, limit, skip }) => {
|
|
4845
4961
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
4846
|
-
return client.get(`/
|
|
4962
|
+
return client.get(`/proposals/document`, {
|
|
4847
4963
|
params: {
|
|
4848
4964
|
locationId: resolvedLocationId,
|
|
4849
|
-
|
|
4850
|
-
|
|
4965
|
+
status,
|
|
4966
|
+
paymentStatus,
|
|
4967
|
+
query,
|
|
4968
|
+
dateFrom,
|
|
4969
|
+
dateTo,
|
|
4970
|
+
limit: Math.min(limit ?? 20, DOC_PAGE_MAX),
|
|
4971
|
+
skip
|
|
4851
4972
|
}
|
|
4852
4973
|
});
|
|
4853
4974
|
}
|
|
@@ -4855,47 +4976,147 @@ function registerDocumentTools(server2, client) {
|
|
|
4855
4976
|
safeTool(
|
|
4856
4977
|
server2,
|
|
4857
4978
|
"get_document",
|
|
4858
|
-
"Retrieve a single
|
|
4979
|
+
"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.",
|
|
4859
4980
|
{
|
|
4860
|
-
documentId: import_zod32.z.string().describe("The document ID to retrieve."),
|
|
4981
|
+
documentId: import_zod32.z.string().describe("The document ID to retrieve (the document's _id)."),
|
|
4861
4982
|
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
|
|
4862
4983
|
},
|
|
4863
4984
|
async ({ documentId, locationId: locationId2 }) => {
|
|
4864
4985
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4986
|
+
let skip = 0;
|
|
4987
|
+
let total = Infinity;
|
|
4988
|
+
let scannedAll = true;
|
|
4989
|
+
for (let page = 0; page < DOC_SCAN_MAX_PAGES && skip < total; page++) {
|
|
4990
|
+
const res = await client.get(`/proposals/document`, {
|
|
4991
|
+
params: { locationId: resolvedLocationId, limit: DOC_PAGE_MAX, skip }
|
|
4992
|
+
});
|
|
4993
|
+
const docs = res?.documents ?? [];
|
|
4994
|
+
if (typeof res?.total === "number") total = res.total;
|
|
4995
|
+
const match = docs.find(
|
|
4996
|
+
(d) => d._id === documentId || d.documentId === documentId || d.id === documentId
|
|
4997
|
+
);
|
|
4998
|
+
if (match) return match;
|
|
4999
|
+
if (docs.length < DOC_PAGE_MAX) break;
|
|
5000
|
+
skip += DOC_PAGE_MAX;
|
|
5001
|
+
if (page === DOC_SCAN_MAX_PAGES - 1 && skip < total) scannedAll = false;
|
|
5002
|
+
}
|
|
5003
|
+
return {
|
|
5004
|
+
found: false,
|
|
5005
|
+
documentId,
|
|
5006
|
+
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).`
|
|
5007
|
+
};
|
|
5008
|
+
}
|
|
5009
|
+
);
|
|
5010
|
+
safeTool(
|
|
5011
|
+
server2,
|
|
5012
|
+
"send_document",
|
|
5013
|
+
"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.",
|
|
5014
|
+
{
|
|
5015
|
+
documentId: import_zod32.z.string().describe("The document ID to send."),
|
|
5016
|
+
sentBy: import_zod32.z.string().optional().describe("GHL user ID of the sender (required by the API; falls back to GHL_USER_ID env)."),
|
|
5017
|
+
documentName: import_zod32.z.string().optional().describe("Override the document name shown to recipients."),
|
|
5018
|
+
medium: import_zod32.z.string().optional().describe("Delivery medium, e.g. 'email'. Defaults to email."),
|
|
5019
|
+
ccRecipients: import_zod32.z.array(import_zod32.z.record(import_zod32.z.unknown())).optional().describe("Optional CC recipients: [{id, email, contactName, firstName, lastName, imageUrl}]."),
|
|
5020
|
+
notificationSettings: import_zod32.z.record(import_zod32.z.unknown()).optional().describe("Optional sender/receiver notification settings: {sender:{fromName,fromEmail}, receive:{subject,templateId}}."),
|
|
5021
|
+
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
|
|
5022
|
+
},
|
|
5023
|
+
async ({ documentId, sentBy, documentName, medium, ccRecipients, notificationSettings, locationId: locationId2 }) => {
|
|
5024
|
+
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
5025
|
+
const resolvedSentBy = sentBy ?? process.env.GHL_USER_ID;
|
|
5026
|
+
if (!resolvedSentBy) {
|
|
5027
|
+
throw new Error(
|
|
5028
|
+
"sentBy (sender user ID) is required. Provide it as a parameter or set GHL_USER_ID in your env."
|
|
5029
|
+
);
|
|
5030
|
+
}
|
|
5031
|
+
const body = {
|
|
5032
|
+
locationId: resolvedLocationId,
|
|
5033
|
+
documentId,
|
|
5034
|
+
sentBy: resolvedSentBy
|
|
5035
|
+
};
|
|
5036
|
+
if (documentName !== void 0) body.documentName = documentName;
|
|
5037
|
+
if (medium !== void 0) body.medium = medium;
|
|
5038
|
+
if (ccRecipients !== void 0) body.ccRecipients = ccRecipients;
|
|
5039
|
+
if (notificationSettings !== void 0) body.notificationSettings = notificationSettings;
|
|
5040
|
+
return client.post(`/proposals/document/send`, { body });
|
|
4868
5041
|
}
|
|
4869
5042
|
);
|
|
4870
5043
|
safeTool(
|
|
4871
5044
|
server2,
|
|
4872
5045
|
"delete_document",
|
|
4873
|
-
"Delete a
|
|
5046
|
+
"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.",
|
|
4874
5047
|
{
|
|
4875
|
-
documentId: import_zod32.z.string().describe("The document ID to delete."),
|
|
5048
|
+
documentId: import_zod32.z.string().describe("The document ID you intend to delete/void."),
|
|
4876
5049
|
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
|
|
4877
5050
|
},
|
|
4878
|
-
async ({ documentId
|
|
5051
|
+
async ({ documentId }) => {
|
|
5052
|
+
return {
|
|
5053
|
+
supported: false,
|
|
5054
|
+
documentId,
|
|
5055
|
+
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."
|
|
5056
|
+
};
|
|
5057
|
+
}
|
|
5058
|
+
);
|
|
5059
|
+
safeTool(
|
|
5060
|
+
server2,
|
|
5061
|
+
"list_document_templates",
|
|
5062
|
+
"List Documents & Contracts TEMPLATES (the reusable contract templates you build, e.g. agreements you send to clients). Backed by GET /proposals/templates.",
|
|
5063
|
+
{
|
|
5064
|
+
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env."),
|
|
5065
|
+
type: import_zod32.z.string().optional().describe("Filter by template type (e.g. 'proposal')."),
|
|
5066
|
+
name: import_zod32.z.string().optional().describe("Filter by template name (partial match)."),
|
|
5067
|
+
isPublicDocument: import_zod32.z.boolean().optional().describe("Filter for public templates only."),
|
|
5068
|
+
userId: import_zod32.z.string().optional().describe("Filter by the user who created the template."),
|
|
5069
|
+
dateFrom: import_zod32.z.string().optional().describe("Only templates updated on/after this date (ISO 8601)."),
|
|
5070
|
+
dateTo: import_zod32.z.string().optional().describe("Only templates updated on/before this date (ISO 8601)."),
|
|
5071
|
+
limit: import_zod32.z.number().optional().describe("Max templates to return. Defaults to 20."),
|
|
5072
|
+
skip: import_zod32.z.number().optional().describe("Number of records to skip for pagination.")
|
|
5073
|
+
},
|
|
5074
|
+
async ({ locationId: locationId2, type, name, isPublicDocument, userId, dateFrom, dateTo, limit, skip }) => {
|
|
4879
5075
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
4880
|
-
return client.
|
|
4881
|
-
params: {
|
|
5076
|
+
return client.get(`/proposals/templates`, {
|
|
5077
|
+
params: {
|
|
5078
|
+
locationId: resolvedLocationId,
|
|
5079
|
+
type,
|
|
5080
|
+
name,
|
|
5081
|
+
isPublicDocument,
|
|
5082
|
+
userId,
|
|
5083
|
+
dateFrom,
|
|
5084
|
+
dateTo,
|
|
5085
|
+
limit: limit ?? 20,
|
|
5086
|
+
skip
|
|
5087
|
+
}
|
|
4882
5088
|
});
|
|
4883
5089
|
}
|
|
4884
5090
|
);
|
|
4885
5091
|
safeTool(
|
|
4886
5092
|
server2,
|
|
4887
|
-
"
|
|
4888
|
-
"
|
|
5093
|
+
"send_document_template",
|
|
5094
|
+
"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.",
|
|
4889
5095
|
{
|
|
4890
|
-
|
|
4891
|
-
contactId: import_zod32.z.string().describe("The contact ID to send the
|
|
5096
|
+
templateId: import_zod32.z.string().describe("The template ID to send (from list_document_templates)."),
|
|
5097
|
+
contactId: import_zod32.z.string().describe("The contact ID to send the contract to."),
|
|
5098
|
+
userId: import_zod32.z.string().optional().describe("GHL user ID of the sender (required by the API; falls back to GHL_USER_ID env)."),
|
|
5099
|
+
sendDocument: import_zod32.z.boolean().optional().describe("If true (default), actually dispatch the document. If false, only create the draft."),
|
|
5100
|
+
opportunityId: import_zod32.z.string().optional().describe("Optionally link the document to an opportunity."),
|
|
4892
5101
|
locationId: import_zod32.z.string().optional().describe("GHL Location ID. Optional if GHL_LOCATION_ID is set in env.")
|
|
4893
5102
|
},
|
|
4894
|
-
async ({
|
|
5103
|
+
async ({ templateId, contactId, userId, sendDocument, opportunityId, locationId: locationId2 }) => {
|
|
4895
5104
|
const resolvedLocationId = client.resolveLocationId(locationId2);
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
5105
|
+
const resolvedUserId = userId ?? process.env.GHL_USER_ID;
|
|
5106
|
+
if (!resolvedUserId) {
|
|
5107
|
+
throw new Error(
|
|
5108
|
+
"userId (sender user ID) is required. Provide it as a parameter or set GHL_USER_ID in your env."
|
|
5109
|
+
);
|
|
5110
|
+
}
|
|
5111
|
+
const body = {
|
|
5112
|
+
templateId,
|
|
5113
|
+
contactId,
|
|
5114
|
+
userId: resolvedUserId,
|
|
5115
|
+
locationId: resolvedLocationId
|
|
5116
|
+
};
|
|
5117
|
+
if (sendDocument !== void 0) body.sendDocument = sendDocument;
|
|
5118
|
+
if (opportunityId !== void 0) body.opportunityId = opportunityId;
|
|
5119
|
+
return client.post(`/proposals/templates/send`, { body });
|
|
4899
5120
|
}
|
|
4900
5121
|
);
|
|
4901
5122
|
}
|
|
@@ -5052,15 +5273,25 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
5052
5273
|
name: import_zod33.z.string().optional().describe("New workflow name."),
|
|
5053
5274
|
status: import_zod33.z.string().optional().describe("New status: 'draft' or 'published'."),
|
|
5054
5275
|
actions: import_zod33.z.array(ActionSchema).optional().describe("Array of workflow actions/steps. For linear flows, provide in order \u2014 chaining is automatic."),
|
|
5055
|
-
triggers: import_zod33.z.array(WorkflowTriggerSchema).optional().describe("Array of workflow triggers.")
|
|
5056
|
-
|
|
5057
|
-
|
|
5276
|
+
triggers: import_zod33.z.array(WorkflowTriggerSchema).optional().describe("Array of workflow triggers."),
|
|
5277
|
+
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."),
|
|
5278
|
+
stopOnResponse: import_zod33.z.boolean().optional().describe("Stop the workflow when the contact replies. If omitted, the current value is preserved."),
|
|
5279
|
+
autoMarkAsRead: import_zod33.z.boolean().optional().describe("Auto-mark conversations as read. If omitted, the current value is preserved."),
|
|
5280
|
+
removeContactFromLastStep: import_zod33.z.boolean().optional().describe("Remove the contact when they reach the last step. If omitted, the current value is preserved."),
|
|
5281
|
+
allowMultipleOpportunity: import_zod33.z.boolean().optional().describe("Allow creating multiple opportunities. If omitted, the current value is preserved.")
|
|
5282
|
+
},
|
|
5283
|
+
async ({ workflowId, name, status, actions, triggers, allowMultiple, stopOnResponse, autoMarkAsRead, removeContactFromLastStep, allowMultipleOpportunity }) => {
|
|
5058
5284
|
try {
|
|
5059
5285
|
const result = await client.updateWorkflow(workflowId, {
|
|
5060
5286
|
name,
|
|
5061
5287
|
status,
|
|
5062
5288
|
actions,
|
|
5063
|
-
triggers
|
|
5289
|
+
triggers,
|
|
5290
|
+
allowMultiple,
|
|
5291
|
+
stopOnResponse,
|
|
5292
|
+
autoMarkAsRead,
|
|
5293
|
+
removeContactFromLastStep,
|
|
5294
|
+
allowMultipleOpportunity
|
|
5064
5295
|
});
|
|
5065
5296
|
return jsonResponse(result);
|
|
5066
5297
|
} catch (error) {
|
|
@@ -8602,24 +8833,58 @@ var publicApiTools = [
|
|
|
8602
8833
|
[registerAccountExportTools, "account-export"],
|
|
8603
8834
|
[registerTemplateDeployerTools, "template-deployer"]
|
|
8604
8835
|
];
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8836
|
+
var internalApiTools = [
|
|
8837
|
+
[registerWorkflowBuilderTools, "workflow-builder"],
|
|
8838
|
+
[registerFunnelBuilderTools, "funnel-builder"],
|
|
8839
|
+
[registerFormBuilderTools, "form-builder"],
|
|
8840
|
+
[registerPipelineBuilderTools, "pipeline-builder"],
|
|
8841
|
+
[registerWorkflowClonerTools, "workflow-cloner"],
|
|
8842
|
+
[registerSmartListTools, "smart-lists"],
|
|
8843
|
+
[registerReputationTools, "reputation"],
|
|
8844
|
+
[registerEmailCampaignTools, "email-campaigns"],
|
|
8845
|
+
[registerEmailBuilderInternalTools, "email-builder"],
|
|
8846
|
+
[registerMembershipTools, "memberships"]
|
|
8847
|
+
];
|
|
8848
|
+
var VALIDATORS_MODULE = "validators";
|
|
8849
|
+
var DIAGNOSTICS_MODULE = "diagnostics";
|
|
8850
|
+
var LOCATION_SWITCHER_MODULE = "location-switcher";
|
|
8851
|
+
var KNOWN_MODULES = /* @__PURE__ */ new Set([
|
|
8852
|
+
...publicApiTools.map(([, label]) => label),
|
|
8853
|
+
...internalApiTools.map(([, label]) => label),
|
|
8854
|
+
VALIDATORS_MODULE,
|
|
8855
|
+
DIAGNOSTICS_MODULE,
|
|
8856
|
+
LOCATION_SWITCHER_MODULE
|
|
8857
|
+
]);
|
|
8858
|
+
function registerAllTools(server2, client, registry2, mcpVersion, env = process.env) {
|
|
8859
|
+
const config3 = parseAllowlist(env);
|
|
8860
|
+
const attemptedTools = /* @__PURE__ */ new Set();
|
|
8861
|
+
const registeredTools = /* @__PURE__ */ new Set();
|
|
8862
|
+
const wrap = (moduleName) => wrapServerForModule(server2, moduleName, config3, attemptedTools, registeredTools);
|
|
8863
|
+
for (const [register, moduleName] of publicApiTools) {
|
|
8864
|
+
register(wrap(moduleName), client);
|
|
8608
8865
|
}
|
|
8609
8866
|
const builderClient = WorkflowBuilderClient.fromEnv(registry2) ?? WorkflowBuilderClient.fromFirstCompany(registry2);
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8867
|
+
for (const [register, moduleName] of internalApiTools) {
|
|
8868
|
+
register(wrap(moduleName), builderClient);
|
|
8869
|
+
}
|
|
8870
|
+
registerValidatorTools(wrap(VALIDATORS_MODULE), client, builderClient);
|
|
8871
|
+
registerDiagnosticTools(
|
|
8872
|
+
wrap(DIAGNOSTICS_MODULE),
|
|
8873
|
+
mcpVersion ?? "unknown",
|
|
8874
|
+
client,
|
|
8875
|
+
builderClient,
|
|
8876
|
+
registry2 ?? null
|
|
8877
|
+
);
|
|
8878
|
+
registerLocationSwitcherTools(
|
|
8879
|
+
wrap(LOCATION_SWITCHER_MODULE),
|
|
8880
|
+
client,
|
|
8881
|
+
builderClient,
|
|
8882
|
+
registry2,
|
|
8883
|
+
mcpVersion
|
|
8884
|
+
);
|
|
8885
|
+
if (isAllowlistActive(config3)) {
|
|
8886
|
+
validateAndLog(config3, KNOWN_MODULES, attemptedTools, registeredTools);
|
|
8887
|
+
}
|
|
8623
8888
|
}
|
|
8624
8889
|
|
|
8625
8890
|
// src/attestation.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.29.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",
|