@elitedcs/ghl-mcp 3.55.0 → 3.56.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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.56.0 — Security off-switches: any tool can be disabled; sub-account create/delete is opt-in
4
+
5
+ Shaped directly by subscriber feedback: some teams keep account-level
6
+ operations human-only, so a prompt-injected or misbehaving AI session can
7
+ never touch them. Two new controls, both enforced at server startup from
8
+ your config — there is deliberately NO way to change them from inside a
9
+ Claude conversation.
10
+
11
+ - **Sub-account create/delete is now OFF by default.** `create_sub_account`
12
+ and `delete_sub_account` register only when `GHL_ENABLE_ACCOUNT_ADMIN=1`
13
+ is set. The setup wizard asks (default No). The allowlist cannot bypass
14
+ the flag. If you were using these tools, add the flag and restart.
15
+ - **Universal deny-list**: `GHL_DISABLED_TOOLS` / `GHL_DISABLED_MODULES`
16
+ switch off any tool or module. A disabled tool never registers — it is
17
+ invisible to the model, so it can't be called *or discovered* from a
18
+ conversation. Deny wins over the allowlist and the always-on set,
19
+ including the credential helpers (`enable_workflow_builder`,
20
+ `auto_capture_firebase_script`, `capture_firebase_interactive`) and
21
+ `install_skills` (denying it also skips the bundled-skills auto-install).
22
+ - **Un-brickable**: the recovery core (`setup_ghl_mcp`, `request_license`,
23
+ `get_mcp_version`, `health_check`) ignores denial, with a logged warning.
24
+ - **Verifiable**: `health_check` gained a "Tool gating" section reporting
25
+ exactly what's disabled, whether account admin is on, and any misspelled
26
+ name in your gating vars (typos do NOT disable anything — they warn).
27
+ - Advertised default tool count is now 233 (235 with account admin enabled).
28
+ - README: new "Turning tools off (security)" section with examples.
29
+
3
30
  ## 3.55.0 — Skills ship inside the package: Blueprint installs itself
4
31
 
5
32
  The gap between "the tools exist" and "the guided experience exists" is
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GHL Command — GoHighLevel MCP Server
2
2
 
3
- **Full GoHighLevel API access for Claude.** 235 tools across 49 modules — manage contacts, conversations, pipelines, calendars, funnels, workflows, invoices, custom objects, webhooks, and more. **Includes full workflow builder, funnel/page editor, form builder, pipeline builder, bulk operations, account export, and workflow cloning** — capabilities no other GHL tool offers. **Multi-tenant:** one install can run the workflow builder across multiple clients' GHL accounts.
3
+ **Full GoHighLevel API access for Claude.** 233 tools across 49 modules — manage contacts, conversations, pipelines, calendars, funnels, workflows, invoices, custom objects, webhooks, and more. **Includes full workflow builder, funnel/page editor, form builder, pipeline builder, bulk operations, account export, and workflow cloning** — capabilities no other GHL tool offers. **Multi-tenant:** one install can run the workflow builder across multiple clients' GHL accounts.
4
4
 
5
5
  **Distributed via npm as [`@elitedcs/ghl-mcp`](https://www.npmjs.com/package/@elitedcs/ghl-mcp).** Buyers install with one config block — no git, no Node.js setup, no terminal commands. Updates flow automatically (`npx @latest` re-resolves on every Claude restart).
6
6
 
@@ -122,7 +122,7 @@ Run setup_ghl_mcp to activate GHL Command:
122
122
  ghl_location_id: YOUR_LOCATION_ID
123
123
  ```
124
124
 
125
- Approve the tool call. Server validates your license, verifies your GHL credentials, writes them to a per-user config file. **Quit Claude one more time and reopen** — the full core toolset is now unlocked (235 tools total with the optional Workflow Builder Firebase add-on).
125
+ Approve the tool call. Server validates your license, verifies your GHL credentials, writes them to a per-user config file. **Quit Claude one more time and reopen** — the full core toolset is now unlocked (233 tools total with the optional Workflow Builder Firebase add-on).
126
126
 
127
127
  ### 4. Try it
128
128
 
@@ -162,7 +162,7 @@ https://app.gohighlevel.com/v2/location/YOUR_LOCATION_ID/dashboard
162
162
 
163
163
  ## Enable the Workflow Builder
164
164
 
165
- The builder + cloner + validator tools (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, validate_workflow) use GHL's internal API and require Firebase credentials. Without them, the other 181 tools work fine — you just won't have workflow/funnel/form/pipeline editing. This is the flagship feature: do this step.
165
+ The builder + cloner + validator tools (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, validate_workflow) use GHL's internal API and require Firebase credentials. Without them, the other 179 tools work fine — you just won't have workflow/funnel/form/pipeline editing. This is the flagship feature: do this step.
166
166
 
167
167
  **One-click capture (v3.49.0+, the supported path):**
168
168
 
@@ -751,7 +751,7 @@ Source repo is private. Contributors need an invitation from `drjerryrelth`. The
751
751
 
752
752
  ### Reducing context / token usage
753
753
 
754
- Every registered MCP tool's schema is shipped to the model on every message. With 235 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`:
754
+ Every registered MCP tool's schema is shipped to the model on every message. With 233 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`:
755
755
 
756
756
  ```jsonc
757
757
  // Claude Desktop config — enable whole modules
@@ -786,6 +786,38 @@ Rules of the road:
786
786
 
787
787
  `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`
788
788
 
789
+ ### Turning tools off (security)
790
+
791
+ Some teams keep certain operations human-only — for example sub-account management or messaging — so that a prompt-injected or misbehaving AI session can never touch them. Two controls exist for exactly that:
792
+
793
+ **1. Sub-account create/delete is OFF by default.** `create_sub_account` and `delete_sub_account` do not register unless you explicitly opt in:
794
+
795
+ ```jsonc
796
+ "env": {
797
+ "GHL_ENABLE_ACCOUNT_ADMIN": "1" // exactly "1" — nothing else enables it
798
+ }
799
+ ```
800
+
801
+ The setup wizard asks this question during install (default No). Listing these tools in `GHL_ENABLED_TOOLS` does **not** bypass the flag.
802
+
803
+ **2. Any tool or module can be switched off** with a deny-list:
804
+
805
+ ```jsonc
806
+ "env": {
807
+ "GHL_DISABLED_TOOLS": "send_message,bulk_delete_contacts",
808
+ "GHL_DISABLED_MODULES": "snapshots"
809
+ }
810
+ ```
811
+
812
+ How the deny-list behaves:
813
+
814
+ - **Deny wins over everything** — the allowlist, the always-on set, and `GHL_ENABLE_ACCOUNT_ADMIN`. A denied tool **never registers**: it is invisible to the model, so it cannot be called *or discovered* from a conversation.
815
+ - **There is deliberately no tool to change gating from inside Claude.** Turning something back on requires editing your config file and fully restarting Claude — a human action.
816
+ - **Recovery core stays reachable:** `setup_ghl_mcp`, `request_license`, `get_mcp_version`, and `health_check` ignore denial (with a logged warning) so you can never brick your install. Everything else — including the Firebase credential helpers `enable_workflow_builder`, `auto_capture_firebase_script`, `capture_firebase_interactive`, and `install_skills` (denying it also skips the bundled-skills auto-install) — is deniable.
817
+ - **Verify it took effect:** run `health_check` in Claude. Its "Tool gating" section shows exactly what's disabled, whether account admin is on, and flags any misspelled name in your gating vars (typos do NOT disable anything — they warn instead).
818
+ - Single-location shops that never want cross-account movement can also deny `switch_location` and `register_location`.
819
+ - Honest limit: this gate is enforced at server startup from your config file, so it is as strong as your machine's file permissions. What it guarantees is that nothing *inside a Claude conversation* can widen the tool surface.
820
+
789
821
  ---
790
822
 
791
823
  ## Troubleshooting
package/dist/index.js CHANGED
@@ -1235,7 +1235,7 @@ async function validateFirebase(firebaseKey, refreshToken) {
1235
1235
  function registerSetupTool(server2) {
1236
1236
  server2.tool(
1237
1237
  "setup_ghl_mcp",
1238
- "First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 235 tools (181 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
1238
+ "First-run setup for GHL Command MCP. Validates your license and GHL credentials, then writes them to a per-user credentials file. Restart Claude after this completes to load all 233 tools (179 if you skip the optional Firebase fields; add Firebase later with enable_workflow_builder).",
1239
1239
  {
1240
1240
  email: import_zod40.z.string().email().describe("Email used at purchase."),
1241
1241
  license_key: import_zod40.z.string().min(20).describe("License key from your purchase email."),
@@ -1317,7 +1317,7 @@ Note: Firebase credentials rejected (${fb.error}). Saved without Workflow Builde
1317
1317
  signed_attestation: lic.signedAttestation
1318
1318
  });
1319
1319
  const isFree = lic.tier === "free";
1320
- const toolCount = isFree ? workflowBuilderEnabled ? "107" : "89" : workflowBuilderEnabled ? "235" : "181";
1320
+ const toolCount = isFree ? workflowBuilderEnabled ? "107" : "89" : workflowBuilderEnabled ? "233" : "179";
1321
1321
  const wfLine = workflowBuilderEnabled ? "Workflow Builder: enabled." : "Workflow Builder: not configured (optional).";
1322
1322
  const wfTip = workflowBuilderEnabled ? "" : isFree ? `
1323
1323
  Unlock the account auditor next (the free tier's best tool): say "Unlock the Workflow Builder" \u2014 one browser login enables audit_workflows, validate_workflow, and the full-detail reads.` : "\nTo enable Workflow Builder later (52 extra Firebase-gated tools): run enable_workflow_builder with your three Firebase values. No need to re-enter license/API key/location ID.";
@@ -1347,7 +1347,7 @@ Unlock the account auditor next (the free tier's best tool): say "Unlock the Wor
1347
1347
  function registerEnableWorkflowBuilderTool(server2) {
1348
1348
  server2.tool(
1349
1349
  "enable_workflow_builder",
1350
- "Add Firebase credentials to an existing GHL Command install to unlock 54 additional tools across the internal-API modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, and memberships, plus the pre-deploy validator. On the FREE tier this same login unlocks the read-only auditor suite (audit_workflows, validate_workflow, full-detail workflow/funnel/pipeline reads). Requires you've already run setup_ghl_mcp. EASIEST PATH: run `capture_firebase_interactive` instead \u2014 a Chrome window opens, you log into GHL, zero pasting. Use THIS tool when you have JSON from `auto_capture_firebase_script` (console-paste path) to put in `firebase_paste`, or the three manual DevTools fields. Tool count goes from 181 to 235 after the next Claude restart.",
1350
+ "Add Firebase credentials to an existing GHL Command install to unlock 54 additional tools across the internal-API modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, and memberships, plus the pre-deploy validator. On the FREE tier this same login unlocks the read-only auditor suite (audit_workflows, validate_workflow, full-detail workflow/funnel/pipeline reads). Requires you've already run setup_ghl_mcp. EASIEST PATH: run `capture_firebase_interactive` instead \u2014 a Chrome window opens, you log into GHL, zero pasting. Use THIS tool when you have JSON from `auto_capture_firebase_script` (console-paste path) to put in `firebase_paste`, or the three manual DevTools fields. Tool count goes from 179 to 233 after the next Claude restart.",
1351
1351
  {
1352
1352
  // v3.25.0: one-paste path. Tool runs `auto_capture_firebase_script` to
1353
1353
  // get the console script; the script returns a JSON object that pastes
@@ -1428,7 +1428,7 @@ DevTools steps: https://elitedcs.com/ghl-mcp-firebase`
1428
1428
  "",
1429
1429
  "**You MUST restart Claude before using any workflow-builder tool.** Quit Claude completely (Cmd+Q on Mac, full exit on Windows) and reopen. Without a restart, the workflow builder tools will keep using the OLD Firebase auth from before this call and fail with 401 errors \u2014 even though this tool reported success.",
1430
1430
  "",
1431
- 'After restart, all 235 tools load. Try: "List my workflows in full detail" or "Validate workflow <id>".',
1431
+ 'After restart, all 233 tools load. Try: "List my workflows in full detail" or "Validate workflow <id>".',
1432
1432
  "",
1433
1433
  "Note: Firebase refresh tokens rotate every few weeks. If workflow tools stop working in a few weeks (run `health_check` to confirm Firebase auth: FAIL), run `auto_capture_firebase_script` for fresh values and re-run this tool with the new firebase_paste."
1434
1434
  ].join("\n")
@@ -1524,7 +1524,7 @@ The login in the capture window may belong to the wrong GHL account, or the sess
1524
1524
  "",
1525
1525
  "**You MUST restart Claude before using any workflow-builder tool.** Quit Claude completely (Cmd+Q on Mac, full exit on Windows) and reopen.",
1526
1526
  "",
1527
- 'After restart, all 235 tools load. Try: "List my workflows in full detail".',
1527
+ 'After restart, all 233 tools load. Try: "List my workflows in full detail".',
1528
1528
  "",
1529
1529
  "Future token rotations re-capture silently \u2014 if workflow tools ever 401, just run capture_firebase_interactive again; no window should appear."
1530
1530
  ].join("\n")
@@ -2102,9 +2102,9 @@ var require_package = __commonJS({
2102
2102
  "package.json"(exports2, module2) {
2103
2103
  module2.exports = {
2104
2104
  name: "@elitedcs/ghl-mcp",
2105
- version: "3.55.0",
2105
+ version: "3.56.0",
2106
2106
  mcpName: "io.github.drjerryrelth/ghl-command",
2107
- description: "GoHighLevel MCP Server for Claude. 235 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
2107
+ description: "GoHighLevel MCP Server for Claude. 233 tools \u2014 full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
2108
2108
  main: "dist/index.js",
2109
2109
  bin: {
2110
2110
  "ghl-mcp": "dist/index.js"
@@ -5683,6 +5683,25 @@ var ALWAYS_ON_TOOL_NAMES = /* @__PURE__ */ new Set([
5683
5683
  "enable_workflow_builder",
5684
5684
  "health_check"
5685
5685
  ]);
5686
+ var HARD_CORE_TOOL_NAMES = /* @__PURE__ */ new Set([
5687
+ "setup_ghl_mcp",
5688
+ "request_license",
5689
+ "get_mcp_version",
5690
+ "health_check"
5691
+ ]);
5692
+ var ACCOUNT_ADMIN_TOOLS = /* @__PURE__ */ new Set([
5693
+ "create_sub_account",
5694
+ "delete_sub_account"
5695
+ ]);
5696
+ var OUT_OF_BAND_TOOL_NAMES = /* @__PURE__ */ new Set([
5697
+ "setup_ghl_mcp",
5698
+ "request_license",
5699
+ "get_mcp_version",
5700
+ "enable_workflow_builder",
5701
+ "auto_capture_firebase_script",
5702
+ "capture_firebase_interactive",
5703
+ "install_skills"
5704
+ ]);
5686
5705
  var FREE_TIER_EXTRA_READ_TOOLS = /* @__PURE__ */ new Set([
5687
5706
  "audit_workflows",
5688
5707
  // account-wide silent-failure audit — the free tier's wow moment
@@ -5732,17 +5751,39 @@ function parseList(raw) {
5732
5751
  function parseAllowlist(env) {
5733
5752
  const moduleList = parseList(env.GHL_ENABLED_MODULES);
5734
5753
  const toolList = parseList(env.GHL_ENABLED_TOOLS);
5754
+ const disabledModuleList = parseList(env.GHL_DISABLED_MODULES);
5755
+ const disabledToolList = parseList(env.GHL_DISABLED_TOOLS);
5735
5756
  return {
5736
5757
  enabledModules: moduleList ? new Set(moduleList) : null,
5737
5758
  enabledTools: toolList ? new Set(toolList) : null,
5759
+ disabledModules: disabledModuleList ? new Set(disabledModuleList) : null,
5760
+ disabledTools: disabledToolList ? new Set(disabledToolList) : null,
5761
+ accountAdminEnabled: env.GHL_ENABLE_ACCOUNT_ADMIN === "1",
5738
5762
  rawModuleInput: moduleList,
5739
- rawToolInput: toolList
5763
+ rawToolInput: toolList,
5764
+ rawDisabledModuleInput: disabledModuleList,
5765
+ rawDisabledToolInput: disabledToolList
5740
5766
  };
5741
5767
  }
5742
5768
  function isAllowlistActive(config3) {
5743
5769
  return config3.enabledModules !== null || config3.enabledTools !== null;
5744
5770
  }
5771
+ function isDenyListActive(config3) {
5772
+ return config3.disabledModules !== null || config3.disabledTools !== null;
5773
+ }
5774
+ function isGatingActive(config3) {
5775
+ return isAllowlistActive(config3) || isDenyListActive(config3) || config3.accountAdminEnabled;
5776
+ }
5777
+ function isToolDenied(toolName, moduleName, config3) {
5778
+ if (HARD_CORE_TOOL_NAMES.has(toolName)) return false;
5779
+ const toolDenied = config3.disabledTools?.has(toolName.toLowerCase()) ?? false;
5780
+ const moduleDenied = moduleName.length > 0 && (config3.disabledModules?.has(moduleName.toLowerCase()) ?? false);
5781
+ return toolDenied || moduleDenied;
5782
+ }
5745
5783
  function shouldRegister(toolName, moduleName, config3) {
5784
+ if (HARD_CORE_TOOL_NAMES.has(toolName)) return true;
5785
+ if (isToolDenied(toolName, moduleName, config3)) return false;
5786
+ if (ACCOUNT_ADMIN_TOOLS.has(toolName) && !config3.accountAdminEnabled) return false;
5746
5787
  if (ALWAYS_ON_TOOL_NAMES.has(toolName)) return true;
5747
5788
  if (!isAllowlistActive(config3)) return true;
5748
5789
  const toolLower = toolName.toLowerCase();
@@ -5782,32 +5823,57 @@ function wrapServerForModule(realServer, moduleName, config3, attemptedTools, re
5782
5823
  }
5783
5824
  });
5784
5825
  }
5826
+ function buildGatingReport(config3, knownModules, attemptedTools, registeredTools, outOfBand) {
5827
+ const knownLower = new Set([...knownModules].map((m) => m.toLowerCase()));
5828
+ const knownToolsLower = /* @__PURE__ */ new Set([
5829
+ ...[...attemptedTools].map((t) => t.toLowerCase()),
5830
+ ...[...OUT_OF_BAND_TOOL_NAMES].map((t) => t.toLowerCase())
5831
+ ]);
5832
+ const unknownModules = (raw) => (raw ?? []).filter((m) => !knownLower.has(m));
5833
+ const unknownTools = (raw) => (raw ?? []).filter((t) => !knownToolsLower.has(t));
5834
+ const hardCoreLower = new Set([...HARD_CORE_TOOL_NAMES].map((t) => t.toLowerCase()));
5835
+ return {
5836
+ gatingActive: isGatingActive(config3),
5837
+ allowlistActive: isAllowlistActive(config3),
5838
+ denyListActive: isDenyListActive(config3),
5839
+ accountAdminEnabled: config3.accountAdminEnabled,
5840
+ enabledModules: [...config3.enabledModules ?? []].sort(),
5841
+ enabledTools: [...config3.enabledTools ?? []].sort(),
5842
+ disabledModules: [...config3.disabledModules ?? []].sort(),
5843
+ disabledTools: [...config3.disabledTools ?? []].sort(),
5844
+ ignoredHardCoreDenials: (config3.rawDisabledToolInput ?? []).filter((t) => hardCoreLower.has(t)),
5845
+ unknownEnabledModules: unknownModules(config3.rawModuleInput),
5846
+ unknownEnabledTools: unknownTools(config3.rawToolInput),
5847
+ unknownDisabledModules: unknownModules(config3.rawDisabledModuleInput),
5848
+ unknownDisabledTools: unknownTools(config3.rawDisabledToolInput),
5849
+ outOfBandDenied: [...outOfBand?.denied ?? []].sort(),
5850
+ registeredCount: registeredTools.size + (outOfBand?.registered.size ?? 0),
5851
+ attemptedCount: attemptedTools.size + (outOfBand?.registered.size ?? 0) + (outOfBand?.denied.size ?? 0)
5852
+ };
5853
+ }
5785
5854
  function validateAndLog(config3, knownModules, attemptedTools, registeredTools, log = (m) => process.stderr.write(m)) {
5786
- if (!isAllowlistActive(config3)) return;
5787
- if (config3.rawModuleInput) {
5788
- const knownLower = new Set([...knownModules].map((m) => m.toLowerCase()));
5789
- const unknown = config3.rawModuleInput.filter((m) => !knownLower.has(m));
5790
- if (unknown.length > 0) {
5791
- log(
5792
- `[ghl-mcp] WARNING: GHL_ENABLED_MODULES has unrecognized name(s): ${unknown.join(", ")}. Known modules: ${[...knownModules].sort().join(", ")}.
5793
- `
5794
- );
5855
+ if (!isGatingActive(config3)) return;
5856
+ const report = buildGatingReport(config3, knownModules, attemptedTools, registeredTools);
5857
+ const warnUnknown = (varName, names, hint = "") => {
5858
+ if (names.length > 0) {
5859
+ log(`[ghl-mcp] WARNING: ${varName} has unrecognized name(s): ${names.join(", ")}.${hint}
5860
+ `);
5795
5861
  }
5796
- }
5797
- if (config3.rawToolInput) {
5798
- const attemptedLower = new Set([...attemptedTools].map((t) => t.toLowerCase()));
5799
- const unknown = config3.rawToolInput.filter((t) => !attemptedLower.has(t));
5800
- if (unknown.length > 0) {
5801
- log(
5802
- `[ghl-mcp] WARNING: GHL_ENABLED_TOOLS has unrecognized name(s): ${unknown.join(", ")}.
5862
+ };
5863
+ const modulesHint = ` Known modules: ${[...knownModules].sort().join(", ")}.`;
5864
+ warnUnknown("GHL_ENABLED_MODULES", report.unknownEnabledModules, modulesHint);
5865
+ warnUnknown("GHL_ENABLED_TOOLS", report.unknownEnabledTools);
5866
+ warnUnknown("GHL_DISABLED_MODULES", report.unknownDisabledModules, modulesHint);
5867
+ warnUnknown("GHL_DISABLED_TOOLS", report.unknownDisabledTools);
5868
+ if (report.ignoredHardCoreDenials.length > 0) {
5869
+ log(
5870
+ `[ghl-mcp] WARNING: GHL_DISABLED_TOOLS lists recovery-core tool(s) that cannot be disabled (ignored): ${report.ignoredHardCoreDenials.join(", ")}.
5803
5871
  `
5804
- );
5805
- }
5872
+ );
5806
5873
  }
5807
- const moduleSummary = config3.enabledModules && config3.enabledModules.size > 0 ? `modules=[${[...config3.enabledModules].sort().join(",")}]` : "modules=(none)";
5808
- const toolSummary = config3.enabledTools && config3.enabledTools.size > 0 ? `explicit-tools=[${[...config3.enabledTools].sort().join(",")}]` : "explicit-tools=(none)";
5874
+ const listOr = (label, items) => items.length > 0 ? `${label}=[${items.join(",")}]` : `${label}=(none)`;
5809
5875
  log(
5810
- `[ghl-mcp] Tool allowlist active: registered ${registeredTools.size} of ${attemptedTools.size} tools (${moduleSummary}; ${toolSummary}).
5876
+ `[ghl-mcp] Tool gating active: registered ${registeredTools.size} of ${attemptedTools.size} tools (${listOr("modules", report.enabledModules)}; ${listOr("explicit-tools", report.enabledTools)}; ${listOr("disabled-modules", report.disabledModules)}; ${listOr("disabled-tools", report.disabledTools)}; account-admin=${report.accountAdminEnabled ? "ENABLED" : "off (default)"}).
5811
5877
  `
5812
5878
  );
5813
5879
  }
@@ -14083,10 +14149,10 @@ async function getVersionStatus(installed) {
14083
14149
  }
14084
14150
 
14085
14151
  // src/tools/diagnostics.ts
14086
- function registerDiagnosticTools(server2, installedVersion, client, builderClient, registry2) {
14152
+ function registerDiagnosticTools(server2, installedVersion, client, builderClient, registry2, getGatingReport) {
14087
14153
  server2.tool(
14088
14154
  "health_check",
14089
- "Run a full health check of the GHL Command MCP install. Reports on: npm registry + version status, GHL API key validity, default location reachability, Firebase auth status (workflow builder), and token registry. Returns a structured pass/fail/warn list with detail per check. Use this when something feels broken or after credentials change.",
14155
+ "Run a full health check of the GHL Command MCP install. Reports on: npm registry + version status, GHL API key validity, default location reachability, Firebase auth status (workflow builder), token registry, and the tool-gating policy (disabled tools/modules, account-admin opt-in, typos in gating env vars). Returns a structured pass/fail/warn list with detail per check. Use this when something feels broken or after credentials change.",
14090
14156
  {},
14091
14157
  async () => {
14092
14158
  const checks = [];
@@ -14136,7 +14202,7 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
14136
14202
  })();
14137
14203
  const firebasePromise = (async () => {
14138
14204
  if (!builderClient) {
14139
- return { name: "Firebase auth (workflow builder)", status: "skip", detail: "Not configured. The 49 Firebase-gated tools need Firebase credentials. The other 163 tools work fine without. To add it: run enable_workflow_builder with the three Firebase values from your GHL browser session (see elitedcs.com/ghl-mcp-firebase for DevTools steps). Do NOT put Firebase values as env vars in your Claude Desktop config \u2014 that path is unreliable and is the usual reason this still shows skip after a restart. enable_workflow_builder saves and verifies them for you." };
14205
+ return { name: "Firebase auth (workflow builder)", status: "skip", detail: "Not configured. The 54 Firebase-gated tools need Firebase credentials. The other 179 tools work fine without. To add it: run enable_workflow_builder with the three Firebase values from your GHL browser session (see elitedcs.com/ghl-mcp-firebase for DevTools steps). Do NOT put Firebase values as env vars in your Claude Desktop config \u2014 that path is unreliable and is the usual reason this still shows skip after a restart. enable_workflow_builder saves and verifies them for you." };
14140
14206
  }
14141
14207
  const result = await builderClient.checkAuth();
14142
14208
  const tokenCompany = builderClient.getTokenCompanyId();
@@ -14185,7 +14251,32 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
14185
14251
  } else {
14186
14252
  versionCheck = { name: "npm registry + version", status: "warn", detail: `Installed v${versionStatus.installed} but latest is v${versionStatus.latest}. Quit Claude (Cmd+Q) and reopen to upgrade.` };
14187
14253
  }
14188
- checks.push(versionCheck, apiKeyCheck, locationCheck, firebaseCheck, registryCheck);
14254
+ const gatingCheck = (() => {
14255
+ const r = getGatingReport?.();
14256
+ if (!r) {
14257
+ return { name: "Tool gating", status: "skip", detail: "Gating report unavailable in this mode." };
14258
+ }
14259
+ if (!r.gatingActive) {
14260
+ return { name: "Tool gating", status: "pass", detail: `No gating configured \u2014 all ${r.registeredCount} tools registered. Sub-account create/delete stays OFF unless GHL_ENABLE_ACCOUNT_ADMIN=1 is set.` };
14261
+ }
14262
+ const parts = [`${r.registeredCount} of ${r.attemptedCount} tools registered.`];
14263
+ if (r.disabledTools.length > 0) parts.push(`Disabled tools: ${r.disabledTools.join(", ")}.`);
14264
+ if (r.disabledModules.length > 0) parts.push(`Disabled modules: ${r.disabledModules.join(", ")}.`);
14265
+ if (r.outOfBandDenied.length > 0) parts.push(`Confirmed unregistered (setup/helper tools): ${r.outOfBandDenied.join(", ")}.`);
14266
+ if (r.allowlistActive) parts.push(`Allowlist: modules=[${r.enabledModules.join(",") || "-"}], tools=[${r.enabledTools.join(",") || "-"}].`);
14267
+ parts.push(`Account admin (sub-account create/delete): ${r.accountAdminEnabled ? "ENABLED via GHL_ENABLE_ACCOUNT_ADMIN=1" : "OFF (default)"}.`);
14268
+ const problems = [];
14269
+ if (r.ignoredHardCoreDenials.length > 0) problems.push(`recovery-core tool(s) in GHL_DISABLED_TOOLS cannot be disabled and were IGNORED: ${r.ignoredHardCoreDenials.join(", ")}`);
14270
+ if (r.unknownDisabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_TOOLS (possible typo \u2014 those tools are NOT disabled): ${r.unknownDisabledTools.join(", ")}`);
14271
+ if (r.unknownDisabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_DISABLED_MODULES (possible typo): ${r.unknownDisabledModules.join(", ")}`);
14272
+ if (r.unknownEnabledTools.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_TOOLS: ${r.unknownEnabledTools.join(", ")}`);
14273
+ if (r.unknownEnabledModules.length > 0) problems.push(`unrecognized name(s) in GHL_ENABLED_MODULES: ${r.unknownEnabledModules.join(", ")}`);
14274
+ if (problems.length > 0) {
14275
+ return { name: "Tool gating", status: "warn", detail: `${parts.join(" ")} PROBLEMS: ${problems.join("; ")}. Fix the env var in your Claude config and fully restart Claude.` };
14276
+ }
14277
+ return { name: "Tool gating", status: "pass", detail: parts.join(" ") };
14278
+ })();
14279
+ checks.push(versionCheck, apiKeyCheck, locationCheck, firebaseCheck, registryCheck, gatingCheck);
14189
14280
  const symbols = { pass: "\u2713", fail: "\u2717", warn: "!", skip: "\u2014" };
14190
14281
  const lines = [];
14191
14282
  lines.push("GHL Command \u2014 Health Check");
@@ -17677,7 +17768,7 @@ var KNOWN_MODULES = /* @__PURE__ */ new Set([
17677
17768
  LOCATION_SWITCHER_MODULE,
17678
17769
  SNAPSHOTS_MODULE
17679
17770
  ]);
17680
- function registerAllTools(server2, client, registry2, mcpVersion, env = process.env, tier = "full") {
17771
+ function registerAllTools(server2, client, registry2, mcpVersion, env = process.env, tier = "full", outOfBand) {
17681
17772
  const config3 = parseAllowlist(env);
17682
17773
  const attemptedTools = /* @__PURE__ */ new Set();
17683
17774
  const registeredTools = /* @__PURE__ */ new Set();
@@ -17699,7 +17790,8 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
17699
17790
  mcpVersion ?? "unknown",
17700
17791
  client,
17701
17792
  builderClient,
17702
- registry2 ?? null
17793
+ registry2 ?? null,
17794
+ () => buildGatingReport(config3, KNOWN_MODULES, attemptedTools, registeredTools, outOfBand)
17703
17795
  );
17704
17796
  registerSnapshotTools(wrap(SNAPSHOTS_MODULE), client, registry2);
17705
17797
  registerLocationSwitcherTools(
@@ -17709,7 +17801,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
17709
17801
  registry2,
17710
17802
  mcpVersion
17711
17803
  );
17712
- if (isAllowlistActive(config3)) {
17804
+ if (isGatingActive(config3)) {
17713
17805
  validateAndLog(config3, KNOWN_MODULES, attemptedTools, registeredTools);
17714
17806
  }
17715
17807
  if (tier === "free") {
@@ -17721,6 +17813,26 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
17721
17813
  return { registeredTools, gatedTools };
17722
17814
  }
17723
17815
 
17816
+ // src/out-of-band.ts
17817
+ function makeOutOfBandTracker() {
17818
+ return { registered: /* @__PURE__ */ new Set(), denied: /* @__PURE__ */ new Set() };
17819
+ }
17820
+ function registerOutOfBand(entries, config3, tracker, log = (m) => process.stderr.write(m)) {
17821
+ for (const entry of entries) {
17822
+ if (isToolDenied(entry.name, "", config3)) {
17823
+ tracker.denied.add(entry.name);
17824
+ log(
17825
+ `[ghl-mcp] Tool disabled by GHL_DISABLED_TOOLS: ${entry.name}${entry.deniedNote ? ` (${entry.deniedNote})` : ""}
17826
+ `
17827
+ );
17828
+ continue;
17829
+ }
17830
+ tracker.registered.add(entry.name);
17831
+ entry.register();
17832
+ entry.sideEffect?.();
17833
+ }
17834
+ }
17835
+
17724
17836
  // src/index.ts
17725
17837
  init_credentials_store();
17726
17838
  init_setup_tool();
@@ -18408,6 +18520,9 @@ async function resolveAccessAndRegister() {
18408
18520
  `);
18409
18521
  }
18410
18522
  inBootstrapMode = !apiKey || !locationId || !licenseVerified;
18523
+ const gatingConfig = parseAllowlist(process.env);
18524
+ const outOfBandTracker = makeOutOfBandTracker();
18525
+ outOfBandTracker.registered.add("get_mcp_version");
18411
18526
  if (inBootstrapMode) {
18412
18527
  process.stderr.write(
18413
18528
  `[ghl-mcp] Bootstrap mode.
@@ -18417,23 +18532,52 @@ async function resolveAccessAndRegister() {
18417
18532
  );
18418
18533
  registerSetupTool(server);
18419
18534
  registerLeadCaptureTool(server);
18420
- registerFirebaseCaptureScriptTool(server);
18535
+ registerOutOfBand(
18536
+ [{ name: "auto_capture_firebase_script", register: () => registerFirebaseCaptureScriptTool(server) }],
18537
+ gatingConfig,
18538
+ outOfBandTracker
18539
+ );
18421
18540
  } else {
18422
18541
  const client = new GHLClient({ apiKey, locationId });
18423
- registerAllTools(server, client, registry, pkg.version, process.env, sessionTier);
18424
- registerEnableWorkflowBuilderTool(server);
18425
- registerFirebaseCaptureScriptTool(server);
18426
- registerInteractiveCaptureTool(server);
18427
- registerSkillsTool(server, pkg.version, __dirname);
18428
- try {
18429
- const skillsResult = installBundledSkills({ packageVersion: pkg.version, baseDir: __dirname });
18430
- const line = summarizeInstall(skillsResult);
18431
- if (line) process.stderr.write(`[ghl-mcp] ${line}
18542
+ registerAllTools(server, client, registry, pkg.version, process.env, sessionTier, outOfBandTracker);
18543
+ registerOutOfBand(
18544
+ [
18545
+ // Available in normal mode so buyers can add Firebase later without
18546
+ // re-running setup_ghl_mcp with all 7 fields, and so they can re-grab
18547
+ // a fresh refresh token when it rotates. Registered on the FREE tier too:
18548
+ // the Firebase login powers the read-only auditor tools (audit_workflows,
18549
+ // validate_workflow, *_full reads) — only the write tools stay gated.
18550
+ { name: "enable_workflow_builder", register: () => registerEnableWorkflowBuilderTool(server) },
18551
+ { name: "auto_capture_firebase_script", register: () => registerFirebaseCaptureScriptTool(server) },
18552
+ // One-click Builder unlock — buyer just logs into a Chrome window the
18553
+ // helper opens; also the silent re-capture path for token rotations.
18554
+ { name: "capture_firebase_interactive", register: () => registerInteractiveCaptureTool(server) },
18555
+ // Skills rail (2026-07-28): bundled skills (Blueprint) install themselves
18556
+ // into ~/.claude/skills — subscribers never download anything separately.
18557
+ // Auto-runs on every start (idempotent, never clobbers user edits);
18558
+ // the tool exists for verification/repair. Every tier: local file copy
18559
+ // only. Denying install_skills also skips the auto-install — a buyer who
18560
+ // gated it clearly doesn't want this server writing files into ~/.claude.
18561
+ {
18562
+ name: "install_skills",
18563
+ register: () => registerSkillsTool(server, pkg.version, __dirname),
18564
+ deniedNote: "bundled-skills auto-install skipped too",
18565
+ sideEffect: () => {
18566
+ try {
18567
+ const skillsResult = installBundledSkills({ packageVersion: pkg.version, baseDir: __dirname });
18568
+ const line = summarizeInstall(skillsResult);
18569
+ if (line) process.stderr.write(`[ghl-mcp] ${line}
18432
18570
  `);
18433
- } catch (e) {
18434
- process.stderr.write(`[ghl-mcp] Skills install skipped: ${String(e).slice(0, 120)}
18571
+ } catch (e) {
18572
+ process.stderr.write(`[ghl-mcp] Skills install skipped: ${String(e).slice(0, 120)}
18435
18573
  `);
18436
- }
18574
+ }
18575
+ }
18576
+ }
18577
+ ],
18578
+ gatingConfig,
18579
+ outOfBandTracker
18580
+ );
18437
18581
  if (fileCreds && !process.env.GHL_API_KEY) {
18438
18582
  process.stderr.write(`[ghl-mcp] Loaded credentials from ${credentialsPath()}
18439
18583
  `);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.55.0",
3
+ "version": "3.56.0",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
- "description": "GoHighLevel MCP Server for Claude. 235 tools — full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
5
+ "description": "GoHighLevel MCP Server for Claude. 233 tools — full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "ghl-mcp": "dist/index.js"