@elitedcs/ghl-mcp 3.54.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.
Files changed (27) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +40 -6
  3. package/dist/index.js +485 -179
  4. package/package.json +4 -3
  5. package/skills/blueprint/README.md +27 -0
  6. package/skills/blueprint/SKILL.md +149 -0
  7. package/skills/blueprint/examples/medspa-approval-view.md +92 -0
  8. package/skills/blueprint/examples/medspa-brief.json +52 -0
  9. package/skills/blueprint/examples/medspa-build-plan.json +265 -0
  10. package/skills/blueprint/examples/medspa-dry-run-report.md +67 -0
  11. package/skills/blueprint/examples/sample-approval-view.md +82 -0
  12. package/skills/blueprint/examples/sample-brief.json +13 -0
  13. package/skills/blueprint/examples/sample-build-plan.json +227 -0
  14. package/skills/blueprint/examples/validate-plan.cjs +127 -0
  15. package/skills/blueprint/presets/clinic-launch-a2p.md +39 -0
  16. package/skills/blueprint/presets/clinic-launch-a2p.preset.json +337 -0
  17. package/skills/blueprint/presets/generic-client.md +36 -0
  18. package/skills/blueprint/presets/generic-client.preset.json +255 -0
  19. package/skills/blueprint/presets/med-spa.md +59 -0
  20. package/skills/blueprint/presets/med-spa.preset.json +264 -0
  21. package/skills/blueprint/references/agency-os-detection.md +80 -0
  22. package/skills/blueprint/references/approval-view.md +83 -0
  23. package/skills/blueprint/references/brief-schema.md +45 -0
  24. package/skills/blueprint/references/build-plan-schema.md +52 -0
  25. package/skills/blueprint/references/external-funnel.md +310 -0
  26. package/skills/blueprint/references/intake-question-set.md +141 -0
  27. package/skills/blueprint/references/preset-format.md +130 -0
package/dist/index.js CHANGED
@@ -114,8 +114,8 @@ var init_ghl_client = __esm({
114
114
  Version: version || GHL_API_VERSION
115
115
  };
116
116
  }
117
- buildUrl(path11, params) {
118
- const url = new URL(path11, GHL_BASE_URL);
117
+ buildUrl(path12, params) {
118
+ const url = new URL(path12, GHL_BASE_URL);
119
119
  if (params) {
120
120
  for (const [key, value] of Object.entries(params)) {
121
121
  if (value !== void 0 && value !== null) {
@@ -125,8 +125,8 @@ var init_ghl_client = __esm({
125
125
  }
126
126
  return url.toString();
127
127
  }
128
- async request(method, path11, options = {}, attempt = 0) {
129
- const url = this.buildUrl(path11, options.params);
128
+ async request(method, path12, options = {}, attempt = 0) {
129
+ const url = this.buildUrl(path12, options.params);
130
130
  const headers = this.buildHeaders(options.version);
131
131
  const fetchOptions = {
132
132
  method,
@@ -144,14 +144,14 @@ var init_ghl_client = __esm({
144
144
  } catch (error) {
145
145
  clearTimeout(timeout);
146
146
  if (error instanceof Error && error.name === "AbortError") {
147
- throw new Error(`Request timeout (30s): ${method} ${path11}`);
147
+ throw new Error(`Request timeout (30s): ${method} ${path12}`);
148
148
  }
149
149
  if (!options.noRetry && attempt < MAX_RETRIES) {
150
150
  const delay4 = computeRetryDelay(null, attempt, BASE_DELAY_MS);
151
- process.stderr.write(`[ghl-mcp] Network error on ${method} ${path11}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
151
+ process.stderr.write(`[ghl-mcp] Network error on ${method} ${path12}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
152
152
  `);
153
153
  await new Promise((r) => setTimeout(r, delay4));
154
- return this.request(method, path11, options, attempt + 1);
154
+ return this.request(method, path12, options, attempt + 1);
155
155
  }
156
156
  throw error;
157
157
  } finally {
@@ -159,10 +159,10 @@ var init_ghl_client = __esm({
159
159
  }
160
160
  if (!options.noRetry && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
161
161
  const delay4 = computeRetryDelay(response.headers.get("Retry-After"), attempt, BASE_DELAY_MS);
162
- process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path11}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
162
+ process.stderr.write(`[ghl-mcp] ${response.status} on ${method} ${path12}, retry ${attempt + 1}/${MAX_RETRIES} in ${delay4}ms
163
163
  `);
164
164
  await new Promise((r) => setTimeout(r, delay4));
165
- return this.request(method, path11, options, attempt + 1);
165
+ return this.request(method, path12, options, attempt + 1);
166
166
  }
167
167
  if (!response.ok) {
168
168
  let errorBody = "";
@@ -171,7 +171,7 @@ var init_ghl_client = __esm({
171
171
  } catch {
172
172
  }
173
173
  throw new Error(
174
- `GHL API Error ${response.status} ${response.statusText}: ${method} ${path11}
174
+ `GHL API Error ${response.status} ${response.statusText}: ${method} ${path12}
175
175
  ${errorBody}`
176
176
  );
177
177
  }
@@ -183,20 +183,20 @@ ${errorBody}`
183
183
  return { message: text };
184
184
  }
185
185
  }
186
- async get(path11, options) {
187
- return this.request("GET", path11, options);
186
+ async get(path12, options) {
187
+ return this.request("GET", path12, options);
188
188
  }
189
- async post(path11, options) {
190
- return this.request("POST", path11, options);
189
+ async post(path12, options) {
190
+ return this.request("POST", path12, options);
191
191
  }
192
- async put(path11, options) {
193
- return this.request("PUT", path11, options);
192
+ async put(path12, options) {
193
+ return this.request("PUT", path12, options);
194
194
  }
195
- async patch(path11, options) {
196
- return this.request("PATCH", path11, options);
195
+ async patch(path12, options) {
196
+ return this.request("PATCH", path12, options);
197
197
  }
198
- async delete(path11, options) {
199
- return this.request("DELETE", path11, options);
198
+ async delete(path12, options) {
199
+ return this.request("DELETE", path12, options);
200
200
  }
201
201
  /**
202
202
  * Helper: resolves locationId from args or falls back to default
@@ -998,9 +998,9 @@ function planFromPayload(payload) {
998
998
  return payload?.plan === "command-os" ? "command-os" : "mcp";
999
999
  }
1000
1000
  function legacyDeviceFingerprint() {
1001
- const os5 = require("node:os");
1001
+ const os6 = require("node:os");
1002
1002
  const crypto4 = require("node:crypto");
1003
- const raw = `${os5.hostname()}:${os5.userInfo().username}:${os5.platform()}:${os5.arch()}`;
1003
+ const raw = `${os6.hostname()}:${os6.userInfo().username}:${os6.platform()}:${os6.arch()}`;
1004
1004
  return crypto4.createHash("sha256").update(raw).digest("hex").slice(0, 16);
1005
1005
  }
1006
1006
  function deviceFingerprint(opts) {
@@ -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 234 tools (180 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,11 +1317,11 @@ 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 ? "106" : "88" : workflowBuilderEnabled ? "234" : "180";
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.";
1324
- const freeTip = isFree ? "\n\nFree tier: 106 read-only tools (88 before the one-login auditor unlock). Write tools stay visible but answer with upgrade info instead of acting. Full version ($97/mo founding rate, same key upgrades in place): https://ghlcommand.com" : "";
1324
+ const freeTip = isFree ? "\n\nFree tier: 107 read-only tools (89 before the one-login auditor unlock). Write tools stay visible but answer with upgrade info instead of acting. Full version ($97/mo founding rate, same key upgrades in place): https://ghlcommand.com" : "";
1325
1325
  return {
1326
1326
  content: [{
1327
1327
  type: "text",
@@ -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 180 to 234 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 234 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 234 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.54.0",
2105
+ version: "3.56.0",
2106
2106
  mcpName: "io.github.drjerryrelth/ghl-command",
2107
- description: "GoHighLevel MCP Server for Claude. 234 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"
@@ -2118,7 +2118,8 @@ var require_package = __commonJS({
2118
2118
  "templates/external-funnel/cloudflare-worker.js",
2119
2119
  "templates/external-funnel/README.md",
2120
2120
  "README.md",
2121
- "CHANGELOG.md"
2121
+ "CHANGELOG.md",
2122
+ "skills"
2122
2123
  ],
2123
2124
  scripts: {
2124
2125
  build: "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --packages=external && esbuild src/capture-helper.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/capture-helper.js --packages=external",
@@ -2191,7 +2192,7 @@ function launchCommand(port = 7300) {
2191
2192
  return `npx -y @elitedcs/ghl-mcp@latest dashboard --port=${port}`;
2192
2193
  }
2193
2194
  function installingEntry() {
2194
- return path7.join(__dirname, "index.js");
2195
+ return path8.join(__dirname, "index.js");
2195
2196
  }
2196
2197
  function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2197
2198
  const cmd = launchCommand(port);
@@ -2199,12 +2200,12 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2199
2200
  const systemApps = "/Applications";
2200
2201
  let base = home;
2201
2202
  try {
2202
- fs7.accessSync(systemApps, fs7.constants.W_OK);
2203
+ fs8.accessSync(systemApps, fs8.constants.W_OK);
2203
2204
  base = "";
2204
2205
  } catch {
2205
2206
  }
2206
- const appDir = base ? path7.join(home, "Applications", "Command OS.app") : path7.join(systemApps, "Command OS.app");
2207
- const macOSDir = path7.join(appDir, "Contents", "MacOS");
2207
+ const appDir = base ? path8.join(home, "Applications", "Command OS.app") : path8.join(systemApps, "Command OS.app");
2208
+ const macOSDir = path8.join(appDir, "Contents", "MacOS");
2208
2209
  const script = [
2209
2210
  "#!/bin/bash",
2210
2211
  "# GHL Command \u2014 Command OS launcher (regenerate: ghl-mcp install-launcher)",
@@ -2233,13 +2234,13 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2233
2234
  targetPath: appDir,
2234
2235
  humanLocation: base ? "your personal Applications folder at ~/Applications (Finder \u2192 Go \u2192 Home \u2192 Applications)" : "your Applications folder \u2014 open Finder \u2192 Applications and look for \u201CCommand OS\u201D",
2235
2236
  files: [
2236
- { path: path7.join(macOSDir, "command-os"), contents: script, executable: true },
2237
- { path: path7.join(appDir, "Contents", "Info.plist"), contents: plist, executable: false }
2237
+ { path: path8.join(macOSDir, "command-os"), contents: script, executable: true },
2238
+ { path: path8.join(appDir, "Contents", "Info.plist"), contents: plist, executable: false }
2238
2239
  ]
2239
2240
  };
2240
2241
  }
2241
2242
  if (platform2 === "win32") {
2242
- const target2 = path7.join(home, "Desktop", "Command OS.cmd");
2243
+ const target2 = path8.join(home, "Desktop", "Command OS.cmd");
2243
2244
  return {
2244
2245
  platform: platform2,
2245
2246
  targetPath: target2,
@@ -2252,7 +2253,7 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2252
2253
  ].join("\r\n"), executable: false }]
2253
2254
  };
2254
2255
  }
2255
- const target = path7.join(home, ".local", "share", "applications", "command-os.desktop");
2256
+ const target = path8.join(home, ".local", "share", "applications", "command-os.desktop");
2256
2257
  return {
2257
2258
  platform: platform2,
2258
2259
  targetPath: target,
@@ -2276,11 +2277,11 @@ function planLauncher(platform2, home, port = 7300, entry = installingEntry()) {
2276
2277
  function installLauncher(argv = []) {
2277
2278
  const portArg = argv.find((a) => a.startsWith("--port="));
2278
2279
  const port = portArg ? Number(portArg.split("=")[1]) : 7300;
2279
- const plan = planLauncher(process.platform, os3.homedir(), port, installingEntry());
2280
+ const plan = planLauncher(process.platform, os4.homedir(), port, installingEntry());
2280
2281
  try {
2281
2282
  for (const f of plan.files) {
2282
- fs7.mkdirSync(path7.dirname(f.path), { recursive: true });
2283
- fs7.writeFileSync(f.path, f.contents, { mode: f.executable ? 493 : 420 });
2283
+ fs8.mkdirSync(path8.dirname(f.path), { recursive: true });
2284
+ fs8.writeFileSync(f.path, f.contents, { mode: f.executable ? 493 : 420 });
2284
2285
  }
2285
2286
  } catch (e) {
2286
2287
  process.stderr.write(`
@@ -2300,13 +2301,13 @@ function installLauncher(argv = []) {
2300
2301
  ].join("\n"));
2301
2302
  return 0;
2302
2303
  }
2303
- var fs7, os3, path7;
2304
+ var fs8, os4, path8;
2304
2305
  var init_launcher = __esm({
2305
2306
  "src/launcher.ts"() {
2306
2307
  "use strict";
2307
- fs7 = __toESM(require("fs"));
2308
- os3 = __toESM(require("os"));
2309
- path7 = __toESM(require("path"));
2308
+ fs8 = __toESM(require("fs"));
2309
+ os4 = __toESM(require("os"));
2310
+ path8 = __toESM(require("path"));
2310
2311
  }
2311
2312
  });
2312
2313
 
@@ -2335,10 +2336,10 @@ function stageBlockedBy(stages, stage) {
2335
2336
  return null;
2336
2337
  }
2337
2338
  function claudeBin() {
2338
- const fallback = path8.join(os4.homedir(), ".local", "bin", "claude");
2339
- const fromPath = (process.env.PATH || "").split(path8.delimiter).map((d) => path8.join(d, "claude")).find((p) => {
2339
+ const fallback = path9.join(os5.homedir(), ".local", "bin", "claude");
2340
+ const fromPath = (process.env.PATH || "").split(path9.delimiter).map((d) => path9.join(d, "claude")).find((p) => {
2340
2341
  try {
2341
- fs8.accessSync(p, fs8.constants.X_OK);
2342
+ fs9.accessSync(p, fs9.constants.X_OK);
2342
2343
  return true;
2343
2344
  } catch {
2344
2345
  return false;
@@ -2346,7 +2347,7 @@ function claudeBin() {
2346
2347
  });
2347
2348
  if (fromPath) return fromPath;
2348
2349
  try {
2349
- fs8.accessSync(fallback, fs8.constants.X_OK);
2350
+ fs9.accessSync(fallback, fs9.constants.X_OK);
2350
2351
  return fallback;
2351
2352
  } catch {
2352
2353
  }
@@ -2395,7 +2396,7 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
2395
2396
  const locationName = SANDBOX_ALLOWLIST[locationId2] || opts.name || locationId2;
2396
2397
  const spec = STAGE_SPECS[stage];
2397
2398
  if (!spec) return Promise.reject(new Error(`Stage ${stage} is not powered yet.`));
2398
- return new Promise((resolve5) => {
2399
+ return new Promise((resolve6) => {
2399
2400
  const bin = claudeBin();
2400
2401
  onEvent({ kind: "status", line: `Starting ${spec.name} on ${locationName} (headless Claude, ${spec.allowedTools.length} tools allowed)\u2026` });
2401
2402
  const child = (0, import_child_process2.spawn)(bin, buildClaudeArgs(spec, locationId2, locationName, opts.context), {
@@ -2441,31 +2442,31 @@ function runStage(locationId2, stage, onEvent, opts = {}) {
2441
2442
  if (code !== 0 && !lastText) {
2442
2443
  const outcome2 = { ok: false, error: `claude exited ${code}: ${stderrTail.trim().slice(-200) || "no output"}` };
2443
2444
  onEvent({ kind: "error", line: outcome2.error });
2444
- resolve5(outcome2);
2445
+ resolve6(outcome2);
2445
2446
  return;
2446
2447
  }
2447
2448
  const outcome = parseResultLine(lastText);
2448
2449
  const detail = outcome.error || (outcome.issues?.length ? outcome.issues.join(" \xB7 ") : "") || outcome.summary || "no detail reported";
2449
2450
  const success = outcome.formId ? `Done: form "${outcome.formName}" (${outcome.formId})` : `Done: ${outcome.summary ?? "stage complete"}`;
2450
2451
  onEvent({ kind: "result", line: outcome.ok ? success : `Not passed: ${detail}` });
2451
- resolve5(outcome);
2452
+ resolve6(outcome);
2452
2453
  });
2453
2454
  child.on("error", (err) => {
2454
2455
  clearTimeout(timeout);
2455
2456
  const outcome = { ok: false, error: `could not start claude: ${err.message}` };
2456
2457
  onEvent({ kind: "error", line: outcome.error });
2457
- resolve5(outcome);
2458
+ resolve6(outcome);
2458
2459
  });
2459
2460
  });
2460
2461
  }
2461
- var import_child_process2, fs8, os4, path8, SANDBOX_ALLOWLIST, PROTECTED_LOCATIONS, STAGE_SPECS, HUMAN_GATE_STAGES;
2462
+ var import_child_process2, fs9, os5, path9, SANDBOX_ALLOWLIST, PROTECTED_LOCATIONS, STAGE_SPECS, HUMAN_GATE_STAGES;
2462
2463
  var init_stage_runner = __esm({
2463
2464
  "src/stage-runner.ts"() {
2464
2465
  "use strict";
2465
2466
  import_child_process2 = require("child_process");
2466
- fs8 = __toESM(require("fs"));
2467
- os4 = __toESM(require("os"));
2468
- path8 = __toESM(require("path"));
2467
+ fs9 = __toESM(require("fs"));
2468
+ os5 = __toESM(require("os"));
2469
+ path9 = __toESM(require("path"));
2469
2470
  SANDBOX_ALLOWLIST = {
2470
2471
  JrV2p35O3hY2wqhr2c0T: "MCP Testing",
2471
2472
  jHP5wkYRineXDlzAOEbW: "Blueprint Demo"
@@ -3185,11 +3186,11 @@ __export(dashboard_exports, {
3185
3186
  writeOverlay: () => writeOverlay
3186
3187
  });
3187
3188
  function overlayPath() {
3188
- return path9.join(appDataDir(), "intake-overlay.json");
3189
+ return path10.join(appDataDir(), "intake-overlay.json");
3189
3190
  }
3190
3191
  function readOverlay() {
3191
3192
  try {
3192
- const raw = JSON.parse(fs9.readFileSync(overlayPath(), "utf8"));
3193
+ const raw = JSON.parse(fs10.readFileSync(overlayPath(), "utf8"));
3193
3194
  if (raw && typeof raw === "object") return raw;
3194
3195
  } catch {
3195
3196
  }
@@ -3197,7 +3198,7 @@ function readOverlay() {
3197
3198
  }
3198
3199
  function writeOverlay(layer) {
3199
3200
  ensureAppDataDir();
3200
- fs9.writeFileSync(overlayPath(), JSON.stringify(layer, null, 2), { mode: 384 });
3201
+ fs10.writeFileSync(overlayPath(), JSON.stringify(layer, null, 2), { mode: 384 });
3201
3202
  }
3202
3203
  function recoverStuckStages(state) {
3203
3204
  let recovered = 0;
@@ -3209,11 +3210,11 @@ function recoverStuckStages(state) {
3209
3210
  return { state: { ...state, clients }, recovered };
3210
3211
  }
3211
3212
  function cockpitStatePath() {
3212
- return path9.join(appDataDir(), "cockpit-state.json");
3213
+ return path10.join(appDataDir(), "cockpit-state.json");
3213
3214
  }
3214
3215
  function readCockpitState() {
3215
3216
  try {
3216
- const raw = JSON.parse(fs9.readFileSync(cockpitStatePath(), "utf8"));
3217
+ const raw = JSON.parse(fs10.readFileSync(cockpitStatePath(), "utf8"));
3217
3218
  if (raw && raw.v === 1 && raw.clients && typeof raw.clients === "object") return raw;
3218
3219
  } catch {
3219
3220
  }
@@ -3221,7 +3222,7 @@ function readCockpitState() {
3221
3222
  }
3222
3223
  function writeCockpitState(state) {
3223
3224
  ensureAppDataDir();
3224
- fs9.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
3225
+ fs10.writeFileSync(cockpitStatePath(), JSON.stringify(state, null, 2), { mode: 384 });
3225
3226
  }
3226
3227
  function setStage(state, locationId2, stageIndex, status) {
3227
3228
  if (!Number.isInteger(stageIndex) || stageIndex < 0 || stageIndex >= STAGES.length) throw new Error("bad stage index");
@@ -3313,7 +3314,7 @@ function notify(message) {
3313
3314
  `);
3314
3315
  try {
3315
3316
  ensureAppDataDir();
3316
- fs9.appendFileSync(path9.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
3317
+ fs10.appendFileSync(path10.join(appDataDir(), "cockpit.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
3317
3318
  `);
3318
3319
  } catch {
3319
3320
  }
@@ -4014,17 +4015,17 @@ async function runDashboard(argv) {
4014
4015
  const source = args.url ? { kind: "url", url: String(args.url) } : args.instruction ? { kind: "recorder", instruction: String(args.instruction) } : { kind: "text", text: String(args.transcript ?? "") };
4015
4016
  let recorderPatterns = [];
4016
4017
  if (source.kind === "recorder") {
4017
- recorderPatterns = await new Promise((resolve5) => {
4018
+ recorderPatterns = await new Promise((resolve6) => {
4018
4019
  const ls = (0, import_child_process4.spawn)(claudeBin(), ["mcp", "list"], { stdio: ["ignore", "pipe", "ignore"] });
4019
4020
  let buf = "";
4020
4021
  ls.stdout.on("data", (d) => {
4021
4022
  buf += d.toString();
4022
4023
  });
4023
- ls.on("close", () => resolve5(recorderAllowPatterns(buf)));
4024
- ls.on("error", () => resolve5([]));
4024
+ ls.on("close", () => resolve6(recorderAllowPatterns(buf)));
4025
+ ls.on("error", () => resolve6([]));
4025
4026
  setTimeout(() => {
4026
4027
  ls.kill("SIGKILL");
4027
- resolve5(recorderAllowPatterns(buf));
4028
+ resolve6(recorderAllowPatterns(buf));
4028
4029
  }, 9e4).unref?.();
4029
4030
  });
4030
4031
  if (!recorderPatterns.length) {
@@ -4034,7 +4035,7 @@ async function runDashboard(argv) {
4034
4035
  }
4035
4036
  }
4036
4037
  const out = await prefillFromSource(
4037
- (prompt, tools) => new Promise((resolve5, reject) => {
4038
+ (prompt, tools) => new Promise((resolve6, reject) => {
4038
4039
  const argv2 = ["-p", prompt, "--max-turns", "25", "--disallowedTools", tools.deny];
4039
4040
  if (tools.allow) argv2.push("--allowedTools", tools.allow);
4040
4041
  else argv2.push("--allowedTools", "");
@@ -4043,7 +4044,7 @@ async function runDashboard(argv) {
4043
4044
  child.stdout.on("data", (d) => {
4044
4045
  out2 += d.toString();
4045
4046
  });
4046
- child.on("close", () => resolve5(out2));
4047
+ child.on("close", () => resolve6(out2));
4047
4048
  child.on("error", reject);
4048
4049
  setTimeout(() => child.kill("SIGKILL"), 5 * 60 * 1e3).unref?.();
4049
4050
  }),
@@ -4175,13 +4176,13 @@ async function runDashboard(argv) {
4175
4176
  }
4176
4177
  }
4177
4178
  const review = await generateReview(
4178
- (prompt) => new Promise((resolve5, reject) => {
4179
+ (prompt) => new Promise((resolve6, reject) => {
4179
4180
  const child = (0, import_child_process4.spawn)(claudeBin(), ["-p", prompt, "--allowedTools", "", "--disallowedTools", "Bash,Write,Edit,WebFetch,WebSearch,Task,Read,Glob,Grep", "--max-turns", "6"], { stdio: ["ignore", "pipe", "pipe"] });
4180
4181
  let out = "";
4181
4182
  child.stdout.on("data", (d) => {
4182
4183
  out += d.toString();
4183
4184
  });
4184
- child.on("close", () => resolve5(out));
4185
+ child.on("close", () => resolve6(out));
4185
4186
  child.on("error", reject);
4186
4187
  setTimeout(() => child.kill("SIGKILL"), 6 * 60 * 1e3).unref?.();
4187
4188
  }),
@@ -4354,9 +4355,9 @@ async function runDashboard(argv) {
4354
4355
  res.end();
4355
4356
  });
4356
4357
  try {
4357
- await new Promise((resolve5, reject) => {
4358
+ await new Promise((resolve6, reject) => {
4358
4359
  server2.once("error", reject);
4359
- server2.listen(port, "127.0.0.1", resolve5);
4360
+ server2.listen(port, "127.0.0.1", resolve6);
4360
4361
  });
4361
4362
  } catch (e) {
4362
4363
  const msg2 = e?.code === "EADDRINUSE" ? `Port ${port} is busy with another program. Close it, or start Command OS on a different port.` : `Command OS could not start: ${e instanceof Error ? e.message : String(e)}`;
@@ -4382,18 +4383,18 @@ async function runDashboard(argv) {
4382
4383
  if (adopted) process.stderr.write(` Synced ${adopted} client${adopted === 1 ? "" : "s"} from your GHL (teammate updates).
4383
4384
  `);
4384
4385
  });
4385
- return await new Promise((resolve5) => {
4386
- const stop = () => server2.close(() => resolve5(0));
4386
+ return await new Promise((resolve6) => {
4387
+ const stop = () => server2.close(() => resolve6(0));
4387
4388
  process.on("SIGINT", stop);
4388
4389
  process.on("SIGTERM", stop);
4389
4390
  });
4390
4391
  }
4391
- var fs9, path9, http, import_child_process3, import_child_process4, osmod, STAGES, ACCOUNT_TYPES, activeRun, UPGRADE_MSG;
4392
+ var fs10, path10, http, import_child_process3, import_child_process4, osmod, STAGES, ACCOUNT_TYPES, activeRun, UPGRADE_MSG;
4392
4393
  var init_dashboard = __esm({
4393
4394
  "src/dashboard.ts"() {
4394
4395
  "use strict";
4395
- fs9 = __toESM(require("fs"));
4396
- path9 = __toESM(require("path"));
4396
+ fs10 = __toESM(require("fs"));
4397
+ path10 = __toESM(require("path"));
4397
4398
  http = __toESM(require("http"));
4398
4399
  import_child_process3 = require("child_process");
4399
4400
  init_credentials_store();
@@ -4428,8 +4429,8 @@ var init_dashboard = __esm({
4428
4429
 
4429
4430
  // src/index.ts
4430
4431
  var dotenv2 = __toESM(require("dotenv"));
4431
- var path10 = __toESM(require("path"));
4432
- var fs10 = __toESM(require("fs"));
4432
+ var path11 = __toESM(require("path"));
4433
+ var fs11 = __toESM(require("fs"));
4433
4434
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
4434
4435
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
4435
4436
  init_ghl_client();
@@ -5682,6 +5683,25 @@ var ALWAYS_ON_TOOL_NAMES = /* @__PURE__ */ new Set([
5682
5683
  "enable_workflow_builder",
5683
5684
  "health_check"
5684
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
+ ]);
5685
5705
  var FREE_TIER_EXTRA_READ_TOOLS = /* @__PURE__ */ new Set([
5686
5706
  "audit_workflows",
5687
5707
  // account-wide silent-failure audit — the free tier's wow moment
@@ -5731,17 +5751,39 @@ function parseList(raw) {
5731
5751
  function parseAllowlist(env) {
5732
5752
  const moduleList = parseList(env.GHL_ENABLED_MODULES);
5733
5753
  const toolList = parseList(env.GHL_ENABLED_TOOLS);
5754
+ const disabledModuleList = parseList(env.GHL_DISABLED_MODULES);
5755
+ const disabledToolList = parseList(env.GHL_DISABLED_TOOLS);
5734
5756
  return {
5735
5757
  enabledModules: moduleList ? new Set(moduleList) : null,
5736
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",
5737
5762
  rawModuleInput: moduleList,
5738
- rawToolInput: toolList
5763
+ rawToolInput: toolList,
5764
+ rawDisabledModuleInput: disabledModuleList,
5765
+ rawDisabledToolInput: disabledToolList
5739
5766
  };
5740
5767
  }
5741
5768
  function isAllowlistActive(config3) {
5742
5769
  return config3.enabledModules !== null || config3.enabledTools !== null;
5743
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
+ }
5744
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;
5745
5787
  if (ALWAYS_ON_TOOL_NAMES.has(toolName)) return true;
5746
5788
  if (!isAllowlistActive(config3)) return true;
5747
5789
  const toolLower = toolName.toLowerCase();
@@ -5781,32 +5823,57 @@ function wrapServerForModule(realServer, moduleName, config3, attemptedTools, re
5781
5823
  }
5782
5824
  });
5783
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
+ }
5784
5854
  function validateAndLog(config3, knownModules, attemptedTools, registeredTools, log = (m) => process.stderr.write(m)) {
5785
- if (!isAllowlistActive(config3)) return;
5786
- if (config3.rawModuleInput) {
5787
- const knownLower = new Set([...knownModules].map((m) => m.toLowerCase()));
5788
- const unknown = config3.rawModuleInput.filter((m) => !knownLower.has(m));
5789
- if (unknown.length > 0) {
5790
- log(
5791
- `[ghl-mcp] WARNING: GHL_ENABLED_MODULES has unrecognized name(s): ${unknown.join(", ")}. Known modules: ${[...knownModules].sort().join(", ")}.
5792
- `
5793
- );
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
+ `);
5794
5861
  }
5795
- }
5796
- if (config3.rawToolInput) {
5797
- const attemptedLower = new Set([...attemptedTools].map((t) => t.toLowerCase()));
5798
- const unknown = config3.rawToolInput.filter((t) => !attemptedLower.has(t));
5799
- if (unknown.length > 0) {
5800
- log(
5801
- `[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(", ")}.
5802
5871
  `
5803
- );
5804
- }
5872
+ );
5805
5873
  }
5806
- const moduleSummary = config3.enabledModules && config3.enabledModules.size > 0 ? `modules=[${[...config3.enabledModules].sort().join(",")}]` : "modules=(none)";
5807
- 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)`;
5808
5875
  log(
5809
- `[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)"}).
5810
5877
  `
5811
5878
  );
5812
5879
  }
@@ -8376,16 +8443,16 @@ function registerEmailTools(server2, client) {
8376
8443
  function registerEmailBuilderInternalTools(server2, builderClient) {
8377
8444
  const client = builderClient;
8378
8445
  if (!client) return;
8379
- async function builderRequest(method, path11, body) {
8446
+ async function builderRequest(method, path12, body) {
8380
8447
  const headers = await client.buildHeaders();
8381
- const response = await fetch(`${EMAIL_BUILDER_BASE}${path11}`, {
8448
+ const response = await fetch(`${EMAIL_BUILDER_BASE}${path12}`, {
8382
8449
  method,
8383
8450
  headers,
8384
8451
  body: body ? JSON.stringify(body) : void 0
8385
8452
  });
8386
8453
  if (!response.ok) {
8387
8454
  const text2 = await response.text();
8388
- throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path11}
8455
+ throw new Error(`Email Builder API Error ${response.status}: ${method} /emails/builder${path12}
8389
8456
  ${text2}`);
8390
8457
  }
8391
8458
  const text = await response.text();
@@ -9642,23 +9709,23 @@ var import_zod34 = require("zod");
9642
9709
  function registerFunnelBuilderTools(server2, builderClient) {
9643
9710
  const client = builderClient;
9644
9711
  if (!client) return;
9645
- async function internalGet(path11) {
9646
- return client.request("GET", path11);
9712
+ async function internalGet(path12) {
9713
+ return client.request("GET", path12);
9647
9714
  }
9648
- async function internalPost(path11, body) {
9649
- return client.request("POST", path11, body);
9715
+ async function internalPost(path12, body) {
9716
+ return client.request("POST", path12, body);
9650
9717
  }
9651
- async function internalPut(path11, body) {
9652
- return client.request("PUT", path11, body);
9718
+ async function internalPut(path12, body) {
9719
+ return client.request("PUT", path12, body);
9653
9720
  }
9654
- async function internalDelete(path11) {
9655
- return client.request("DELETE", path11);
9721
+ async function internalDelete(path12) {
9722
+ return client.request("DELETE", path12);
9656
9723
  }
9657
- async function funnelRequest(method, path11, body) {
9724
+ async function funnelRequest(method, path12, body) {
9658
9725
  const headers = await client.buildHeaders();
9659
9726
  headers.Origin = "https://app.gohighlevel.com";
9660
9727
  headers.Referer = "https://app.gohighlevel.com/";
9661
- const url = `https://backend.leadconnectorhq.com/funnels${path11}`;
9728
+ const url = `https://backend.leadconnectorhq.com/funnels${path12}`;
9662
9729
  const options = { method, headers };
9663
9730
  if (body && (method === "POST" || method === "PUT")) {
9664
9731
  options.body = JSON.stringify(body);
@@ -9666,7 +9733,7 @@ function registerFunnelBuilderTools(server2, builderClient) {
9666
9733
  const response = await fetch(url, options);
9667
9734
  if (!response.ok) {
9668
9735
  const text2 = await response.text();
9669
- throw new Error(`Funnel API Error ${response.status}: ${method} ${path11}
9736
+ throw new Error(`Funnel API Error ${response.status}: ${method} ${path12}
9670
9737
  ${text2}`);
9671
9738
  }
9672
9739
  const text = await response.text();
@@ -10611,12 +10678,12 @@ var valueCardSchema = import_zod35.z.object({
10611
10678
  function registerPageStudioTools(server2, builderClient) {
10612
10679
  const client = builderClient;
10613
10680
  if (!client) return;
10614
- async function funnelRequest(method, path11) {
10681
+ async function funnelRequest(method, path12) {
10615
10682
  const headers = await client.buildHeaders();
10616
10683
  headers.Origin = "https://app.gohighlevel.com";
10617
10684
  headers.Referer = "https://app.gohighlevel.com/";
10618
- const response = await fetch(`https://backend.leadconnectorhq.com/funnels${path11}`, { method, headers });
10619
- if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path11}
10685
+ const response = await fetch(`https://backend.leadconnectorhq.com/funnels${path12}`, { method, headers });
10686
+ if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${path12}
10620
10687
  ${await response.text()}`);
10621
10688
  const text = await response.text();
10622
10689
  return text ? JSON.parse(text) : {};
@@ -10935,9 +11002,9 @@ function buildUpdateFormPath(formId, locationId2) {
10935
11002
  function buildUpdateFormBody(name, formData) {
10936
11003
  return { name, formData };
10937
11004
  }
10938
- async function formApiRequest(client, method, path11, body) {
11005
+ async function formApiRequest(client, method, path12, body) {
10939
11006
  const headers = await client.buildHeaders();
10940
- const url = `https://backend.leadconnectorhq.com/forms${path11}`;
11007
+ const url = `https://backend.leadconnectorhq.com/forms${path12}`;
10941
11008
  const options = { method, headers };
10942
11009
  if (body && (method === "POST" || method === "PUT")) {
10943
11010
  options.body = JSON.stringify(body);
@@ -10945,7 +11012,7 @@ async function formApiRequest(client, method, path11, body) {
10945
11012
  const response = await fetch(url, options);
10946
11013
  if (!response.ok) {
10947
11014
  const text2 = await response.text();
10948
- throw new Error(`Form API Error ${response.status}: ${method} ${path11}
11015
+ throw new Error(`Form API Error ${response.status}: ${method} ${path12}
10949
11016
  ${text2}`);
10950
11017
  }
10951
11018
  const text = await response.text();
@@ -10959,7 +11026,7 @@ ${text2}`);
10959
11026
  function registerFormBuilderTools(server2, builderClient, publicClient) {
10960
11027
  const client = builderClient;
10961
11028
  if (!client) return;
10962
- const formRequest = (method, path11, body) => formApiRequest(client, method, path11, body);
11029
+ const formRequest = (method, path12, body) => formApiRequest(client, method, path12, body);
10963
11030
  server2.tool(
10964
11031
  "get_form_full",
10965
11032
  "Get a form with full builder data: all fields (labels, types, IDs, validation), conditional logic, auto-responder config, email notification settings, styling, and version history. This is the internal API \u2014 it returns everything the form builder UI shows.",
@@ -11080,10 +11147,10 @@ function registerFormBuilderTools(server2, builderClient, publicClient) {
11080
11147
  },
11081
11148
  async ({ formId, limit, skip }) => {
11082
11149
  try {
11083
- let path11 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
11084
- if (formId) path11 += `&formId=${formId}`;
11085
- if (skip) path11 += `&skip=${skip}`;
11086
- const result = await formRequest("GET", path11);
11150
+ let path12 = `/submissions?locationId=${client.locationId}&limit=${limit ?? 20}`;
11151
+ if (formId) path12 += `&formId=${formId}`;
11152
+ if (skip) path12 += `&skip=${skip}`;
11153
+ const result = await formRequest("GET", path12);
11087
11154
  return {
11088
11155
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
11089
11156
  };
@@ -11476,9 +11543,9 @@ var import_zod38 = require("zod");
11476
11543
  function registerPipelineBuilderTools(server2, builderClient) {
11477
11544
  const client = builderClient;
11478
11545
  if (!client) return;
11479
- async function pipelineRequest(method, path11, body) {
11546
+ async function pipelineRequest(method, path12, body) {
11480
11547
  const headers = await client.buildHeaders();
11481
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path11}`;
11548
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path12}`;
11482
11549
  const options = { method, headers };
11483
11550
  if (body && (method === "POST" || method === "PUT" || method === "PATCH")) {
11484
11551
  options.body = JSON.stringify(body);
@@ -11486,7 +11553,7 @@ function registerPipelineBuilderTools(server2, builderClient) {
11486
11553
  const response = await fetch(url, options);
11487
11554
  if (!response.ok) {
11488
11555
  const text2 = await response.text();
11489
- throw new Error(`Pipeline API Error ${response.status}: ${method} ${path11}
11556
+ throw new Error(`Pipeline API Error ${response.status}: ${method} ${path12}
11490
11557
  ${text2}`);
11491
11558
  }
11492
11559
  const text = await response.text();
@@ -12231,7 +12298,7 @@ ${lines.join("\n")}
12231
12298
  // src/tools/bulk-operations.ts
12232
12299
  var import_zod42 = require("zod");
12233
12300
  function delay(ms) {
12234
- return new Promise((resolve5) => setTimeout(resolve5, ms));
12301
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
12235
12302
  }
12236
12303
  function formatResults(op, results, total) {
12237
12304
  return `${op}: ${results.success} success, ${results.failed} failed out of ${total}.${results.errors.length ? "\nErrors:\n" + results.errors.join("\n") : ""}`;
@@ -12356,7 +12423,7 @@ function registerBulkOperationTools(server2, client) {
12356
12423
  // src/tools/account-export.ts
12357
12424
  var import_zod43 = require("zod");
12358
12425
  function delay2(ms) {
12359
- return new Promise((resolve5) => setTimeout(resolve5, ms));
12426
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
12360
12427
  }
12361
12428
  function registerAccountExportTools(server2, client) {
12362
12429
  const builderClient = WorkflowBuilderClient.fromEnv();
@@ -12677,9 +12744,9 @@ var OBJECT_KEYS = ["contacts", "opportunity"];
12677
12744
  function registerSmartListTools(server2, builderClient) {
12678
12745
  const client = builderClient;
12679
12746
  if (!client) return;
12680
- async function smartListRequest(method, path11, body) {
12747
+ async function smartListRequest(method, path12, body) {
12681
12748
  const headers = await client.buildHeaders();
12682
- const url = `${SMARTLIST_BASE}${path11}`;
12749
+ const url = `${SMARTLIST_BASE}${path12}`;
12683
12750
  const options = { method, headers };
12684
12751
  if (body && (method === "POST" || method === "PUT")) {
12685
12752
  options.body = JSON.stringify(body);
@@ -12687,7 +12754,7 @@ function registerSmartListTools(server2, builderClient) {
12687
12754
  const response = await fetch(url, options);
12688
12755
  if (!response.ok) {
12689
12756
  const text2 = await response.text();
12690
- throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path11}
12757
+ throw new Error(`Smart Lists API Error ${response.status}: ${method} ${path12}
12691
12758
  ${text2}`);
12692
12759
  }
12693
12760
  const text = await response.text();
@@ -12819,12 +12886,12 @@ var REPUTATION_BASE = "https://backend.leadconnectorhq.com/reputation";
12819
12886
  function registerReputationTools(server2, builderClient) {
12820
12887
  const client = builderClient;
12821
12888
  if (!client) return;
12822
- async function reputationRequest(method, path11) {
12889
+ async function reputationRequest(method, path12) {
12823
12890
  const headers = await client.buildHeaders();
12824
- const response = await fetch(`${REPUTATION_BASE}${path11}`, { method, headers });
12891
+ const response = await fetch(`${REPUTATION_BASE}${path12}`, { method, headers });
12825
12892
  if (!response.ok) {
12826
12893
  const text2 = await response.text();
12827
- throw new Error(`Reputation API Error ${response.status}: ${method} ${path11}
12894
+ throw new Error(`Reputation API Error ${response.status}: ${method} ${path12}
12828
12895
  ${text2}`);
12829
12896
  }
12830
12897
  const text = await response.text();
@@ -12939,16 +13006,16 @@ var MEMBERSHIP_BASE = "https://backend.leadconnectorhq.com/membership";
12939
13006
  function registerMembershipTools(server2, builderClient) {
12940
13007
  const client = builderClient;
12941
13008
  if (!client) return;
12942
- async function membershipRequest(path11, method = "GET", body) {
13009
+ async function membershipRequest(path12, method = "GET", body) {
12943
13010
  const headers = await client.buildHeaders();
12944
- const response = await fetch(`${MEMBERSHIP_BASE}${path11}`, {
13011
+ const response = await fetch(`${MEMBERSHIP_BASE}${path12}`, {
12945
13012
  method,
12946
13013
  headers,
12947
13014
  body: body ? JSON.stringify(body) : void 0
12948
13015
  });
12949
13016
  if (!response.ok) {
12950
13017
  const text2 = await response.text();
12951
- throw new Error(`Membership API Error ${response.status}: ${method} ${path11}
13018
+ throw new Error(`Membership API Error ${response.status}: ${method} ${path12}
12952
13019
  ${text2}`);
12953
13020
  }
12954
13021
  const text = await response.text();
@@ -13117,7 +13184,7 @@ var import_zod49 = require("zod");
13117
13184
  var fs5 = __toESM(require("fs"));
13118
13185
  var path5 = __toESM(require("path"));
13119
13186
  function delay3(ms) {
13120
- return new Promise((resolve5) => setTimeout(resolve5, ms));
13187
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
13121
13188
  }
13122
13189
  var TemplateSchema = import_zod49.z.object({
13123
13190
  templateName: import_zod49.z.string(),
@@ -13261,7 +13328,7 @@ function registerTemplateDeployerTools(server2, client) {
13261
13328
  const locId = client.resolveLocationId(locationId2);
13262
13329
  const safePath = validateTemplatePath(templateFile);
13263
13330
  const template = TemplateSchema.parse(JSON.parse(fs5.readFileSync(safePath, "utf-8")));
13264
- const resolve5 = (text) => {
13331
+ const resolve6 = (text) => {
13265
13332
  if (typeof text !== "string") return text;
13266
13333
  let result = text;
13267
13334
  for (const [key, value] of Object.entries(answers)) {
@@ -13274,7 +13341,7 @@ function registerTemplateDeployerTools(server2, client) {
13274
13341
  return result;
13275
13342
  };
13276
13343
  const resolveObj = (obj) => {
13277
- if (typeof obj === "string") return resolve5(obj);
13344
+ if (typeof obj === "string") return resolve6(obj);
13278
13345
  if (Array.isArray(obj)) return obj.map(resolveObj);
13279
13346
  if (obj && typeof obj === "object") {
13280
13347
  const result = {};
@@ -14082,10 +14149,10 @@ async function getVersionStatus(installed) {
14082
14149
  }
14083
14150
 
14084
14151
  // src/tools/diagnostics.ts
14085
- function registerDiagnosticTools(server2, installedVersion, client, builderClient, registry2) {
14152
+ function registerDiagnosticTools(server2, installedVersion, client, builderClient, registry2, getGatingReport) {
14086
14153
  server2.tool(
14087
14154
  "health_check",
14088
- "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.",
14089
14156
  {},
14090
14157
  async () => {
14091
14158
  const checks = [];
@@ -14135,7 +14202,7 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
14135
14202
  })();
14136
14203
  const firebasePromise = (async () => {
14137
14204
  if (!builderClient) {
14138
- 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." };
14139
14206
  }
14140
14207
  const result = await builderClient.checkAuth();
14141
14208
  const tokenCompany = builderClient.getTokenCompanyId();
@@ -14184,7 +14251,32 @@ function registerDiagnosticTools(server2, installedVersion, client, builderClien
14184
14251
  } else {
14185
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.` };
14186
14253
  }
14187
- 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);
14188
14280
  const symbols = { pass: "\u2713", fail: "\u2717", warn: "!", skip: "\u2014" };
14189
14281
  const lines = [];
14190
14282
  lines.push("GHL Command \u2014 Health Check");
@@ -14897,9 +14989,9 @@ function presetForBusinessType(type) {
14897
14989
  return "generic";
14898
14990
  }
14899
14991
  }
14900
- function setPath(target, path11, value) {
14992
+ function setPath(target, path12, value) {
14901
14993
  if (value === void 0) return;
14902
- const parts = path11.split(".");
14994
+ const parts = path12.split(".");
14903
14995
  let node = target;
14904
14996
  for (let i = 0; i < parts.length - 1; i++) {
14905
14997
  const k = parts[i];
@@ -16985,15 +17077,15 @@ function extractFunnelId(result) {
16985
17077
  return void 0;
16986
17078
  }
16987
17079
  function makeExecuteDeps(client, builderClient, locationId2) {
16988
- const pipelineApi = async (method, path11, body) => {
17080
+ const pipelineApi = async (method, path12, body) => {
16989
17081
  const headers = await builderClient.buildHeaders();
16990
- const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path11}`;
17082
+ const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${path12}`;
16991
17083
  const options = { method, headers };
16992
17084
  if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
16993
17085
  const response = await fetch(url, options);
16994
17086
  if (!response.ok) {
16995
17087
  const text2 = await response.text();
16996
- throw new Error(`Pipeline API ${response.status}: ${method} ${path11}
17088
+ throw new Error(`Pipeline API ${response.status}: ${method} ${path12}
16997
17089
  ${text2.slice(0, 300)}`);
16998
17090
  }
16999
17091
  const text = await response.text();
@@ -17004,17 +17096,17 @@ ${text2.slice(0, 300)}`);
17004
17096
  return JSON.parse(text.replace(/[\x00-\x1F\x7F]/g, ""));
17005
17097
  }
17006
17098
  };
17007
- const funnelApi = async (method, path11, body) => {
17099
+ const funnelApi = async (method, path12, body) => {
17008
17100
  const headers = await builderClient.buildHeaders();
17009
17101
  headers.Origin = "https://app.gohighlevel.com";
17010
17102
  headers.Referer = "https://app.gohighlevel.com/";
17011
- const url = `https://backend.leadconnectorhq.com/funnels${path11}`;
17103
+ const url = `https://backend.leadconnectorhq.com/funnels${path12}`;
17012
17104
  const options = { method, headers };
17013
17105
  if (body && (method === "POST" || method === "PUT")) options.body = JSON.stringify(body);
17014
17106
  const response = await fetch(url, options);
17015
17107
  if (!response.ok) {
17016
17108
  const text2 = await response.text();
17017
- throw new Error(`Funnel API ${response.status}: ${method} ${path11}
17109
+ throw new Error(`Funnel API ${response.status}: ${method} ${path12}
17018
17110
  ${text2.slice(0, 300)}`);
17019
17111
  }
17020
17112
  const text = await response.text();
@@ -17676,7 +17768,7 @@ var KNOWN_MODULES = /* @__PURE__ */ new Set([
17676
17768
  LOCATION_SWITCHER_MODULE,
17677
17769
  SNAPSHOTS_MODULE
17678
17770
  ]);
17679
- function registerAllTools(server2, client, registry2, mcpVersion, env = process.env, tier = "full") {
17771
+ function registerAllTools(server2, client, registry2, mcpVersion, env = process.env, tier = "full", outOfBand) {
17680
17772
  const config3 = parseAllowlist(env);
17681
17773
  const attemptedTools = /* @__PURE__ */ new Set();
17682
17774
  const registeredTools = /* @__PURE__ */ new Set();
@@ -17698,7 +17790,8 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
17698
17790
  mcpVersion ?? "unknown",
17699
17791
  client,
17700
17792
  builderClient,
17701
- registry2 ?? null
17793
+ registry2 ?? null,
17794
+ () => buildGatingReport(config3, KNOWN_MODULES, attemptedTools, registeredTools, outOfBand)
17702
17795
  );
17703
17796
  registerSnapshotTools(wrap(SNAPSHOTS_MODULE), client, registry2);
17704
17797
  registerLocationSwitcherTools(
@@ -17708,7 +17801,7 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
17708
17801
  registry2,
17709
17802
  mcpVersion
17710
17803
  );
17711
- if (isAllowlistActive(config3)) {
17804
+ if (isGatingActive(config3)) {
17712
17805
  validateAndLog(config3, KNOWN_MODULES, attemptedTools, registeredTools);
17713
17806
  }
17714
17807
  if (tier === "free") {
@@ -17720,9 +17813,180 @@ function registerAllTools(server2, client, registry2, mcpVersion, env = process.
17720
17813
  return { registeredTools, gatedTools };
17721
17814
  }
17722
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
+
17723
17836
  // src/index.ts
17724
17837
  init_credentials_store();
17725
17838
  init_setup_tool();
17839
+
17840
+ // src/tools/skills.ts
17841
+ var import_zod57 = require("zod");
17842
+
17843
+ // src/skill-installer.ts
17844
+ var import_node_crypto2 = require("node:crypto");
17845
+ var fs6 = __toESM(require("node:fs"));
17846
+ var path6 = __toESM(require("node:path"));
17847
+ var os3 = __toESM(require("node:os"));
17848
+ function sha256(buf) {
17849
+ return (0, import_node_crypto2.createHash)("sha256").update(buf).digest("hex");
17850
+ }
17851
+ function bundledSkillsDir(baseDir) {
17852
+ const candidate = path6.resolve(baseDir, "..", "skills");
17853
+ try {
17854
+ return fs6.statSync(candidate).isDirectory() ? candidate : null;
17855
+ } catch {
17856
+ return null;
17857
+ }
17858
+ }
17859
+ function listFiles(root) {
17860
+ const out = [];
17861
+ const walk = (dir) => {
17862
+ for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
17863
+ const full = path6.join(dir, entry.name);
17864
+ if (entry.isDirectory()) walk(full);
17865
+ else if (entry.isFile()) out.push(path6.relative(root, full));
17866
+ }
17867
+ };
17868
+ walk(root);
17869
+ return out.sort();
17870
+ }
17871
+ function readMarker(markerPath) {
17872
+ try {
17873
+ const parsed = JSON.parse(fs6.readFileSync(markerPath, "utf8"));
17874
+ return parsed && typeof parsed === "object" && parsed.files ? parsed : null;
17875
+ } catch {
17876
+ return null;
17877
+ }
17878
+ }
17879
+ function installBundledSkills(opts) {
17880
+ const result = {
17881
+ installed: [],
17882
+ updated: [],
17883
+ skippedUserModified: [],
17884
+ unchanged: [],
17885
+ errors: [],
17886
+ targetDir: opts.targetDir ?? path6.join(os3.homedir(), ".claude", "skills"),
17887
+ bundledDir: null
17888
+ };
17889
+ const bundled = bundledSkillsDir(opts.baseDir);
17890
+ result.bundledDir = bundled;
17891
+ if (!bundled) return result;
17892
+ try {
17893
+ fs6.mkdirSync(result.targetDir, { recursive: true });
17894
+ } catch (e) {
17895
+ result.errors.push(`cannot create ${result.targetDir}: ${String(e)}`);
17896
+ return result;
17897
+ }
17898
+ const markerPath = path6.join(result.targetDir, ".ghl-command-skills.json");
17899
+ const marker = readMarker(markerPath) ?? { packageVersion: "", files: {} };
17900
+ const newMarker = { packageVersion: opts.packageVersion, files: { ...marker.files } };
17901
+ for (const rel of listFiles(bundled)) {
17902
+ try {
17903
+ const src = fs6.readFileSync(path6.join(bundled, rel));
17904
+ const dest = path6.join(result.targetDir, rel);
17905
+ const srcHash = sha256(src);
17906
+ let destBuf = null;
17907
+ try {
17908
+ destBuf = fs6.readFileSync(dest);
17909
+ } catch {
17910
+ destBuf = null;
17911
+ }
17912
+ if (destBuf === null) {
17913
+ fs6.mkdirSync(path6.dirname(dest), { recursive: true });
17914
+ fs6.writeFileSync(dest, src);
17915
+ newMarker.files[rel] = srcHash;
17916
+ result.installed.push(rel);
17917
+ continue;
17918
+ }
17919
+ const destHash = sha256(destBuf);
17920
+ if (destHash === srcHash) {
17921
+ newMarker.files[rel] = srcHash;
17922
+ result.unchanged.push(rel);
17923
+ continue;
17924
+ }
17925
+ const lastWritten = marker.files[rel];
17926
+ if (lastWritten && destHash === lastWritten) {
17927
+ fs6.writeFileSync(dest, src);
17928
+ newMarker.files[rel] = srcHash;
17929
+ result.updated.push(rel);
17930
+ } else {
17931
+ result.skippedUserModified.push(rel);
17932
+ }
17933
+ } catch (e) {
17934
+ result.errors.push(`${rel}: ${String(e)}`);
17935
+ }
17936
+ }
17937
+ try {
17938
+ fs6.writeFileSync(markerPath, JSON.stringify(newMarker, null, 2));
17939
+ } catch (e) {
17940
+ result.errors.push(`marker write failed: ${String(e)}`);
17941
+ }
17942
+ return result;
17943
+ }
17944
+ function summarizeInstall(r) {
17945
+ if (!r.bundledDir) return "";
17946
+ const bits = [];
17947
+ if (r.installed.length) bits.push(`${r.installed.length} installed`);
17948
+ if (r.updated.length) bits.push(`${r.updated.length} updated`);
17949
+ if (r.skippedUserModified.length) bits.push(`${r.skippedUserModified.length} kept (your edits)`);
17950
+ if (r.errors.length) bits.push(`${r.errors.length} errors`);
17951
+ if (bits.length === 0) return "";
17952
+ return `Skills: ${bits.join(", ")} \u2192 ${r.targetDir}`;
17953
+ }
17954
+
17955
+ // src/tools/skills.ts
17956
+ function registerSkillsTool(server2, packageVersion, baseDir) {
17957
+ server2.tool(
17958
+ "install_skills",
17959
+ "Install (or repair) the guided skills bundled with GHL Command \u2014 currently the Blueprint skill (build a whole client account from one intake) \u2014 into your ~/.claude/skills/ so Claude can use them. Runs automatically on startup; call this to verify what is installed, or to re-install after deleting a skill. NEVER overwrites files you have edited (your version is kept and reported). Restart Claude after install for new skills to load.",
17960
+ {
17961
+ targetDir: import_zod57.z.string().optional().describe("Override the install directory. Default: ~/.claude/skills")
17962
+ },
17963
+ async ({ targetDir }) => {
17964
+ try {
17965
+ const result = installBundledSkills({ packageVersion, baseDir, targetDir });
17966
+ if (!result.bundledDir) {
17967
+ return jsonResponse({
17968
+ ok: false,
17969
+ note: "No bundled skills directory found in this install (development checkout without skills/, or a pre-3.55.0 package)."
17970
+ });
17971
+ }
17972
+ return jsonResponse({
17973
+ ok: result.errors.length === 0,
17974
+ targetDir: result.targetDir,
17975
+ installed: result.installed,
17976
+ updated: result.updated,
17977
+ keptYourEdits: result.skippedUserModified,
17978
+ unchanged: result.unchanged.length,
17979
+ errors: result.errors,
17980
+ next: result.installed.length > 0 || result.updated.length > 0 ? "Fully restart Claude (quit and reopen) so the new/updated skills load." : "Everything already current."
17981
+ });
17982
+ } catch (error) {
17983
+ return errorResponse(error);
17984
+ }
17985
+ }
17986
+ );
17987
+ }
17988
+
17989
+ // src/index.ts
17726
17990
  init_attestation();
17727
17991
 
17728
17992
  // src/tools/meta.ts
@@ -17756,8 +18020,8 @@ function registerMetaTools(server2, installedVersion) {
17756
18020
 
17757
18021
  // src/cli.ts
17758
18022
  var import_node_util = require("node:util");
17759
- var fs6 = __toESM(require("fs"));
17760
- var path6 = __toESM(require("path"));
18023
+ var fs7 = __toESM(require("fs"));
18024
+ var path7 = __toESM(require("path"));
17761
18025
  var import_crypto2 = require("crypto");
17762
18026
  init_ghl_client();
17763
18027
  init_token_registry();
@@ -17800,9 +18064,9 @@ function errLine(msg2) {
17800
18064
  function preflightWritable() {
17801
18065
  try {
17802
18066
  const dir = ensureAppDataDir();
17803
- const probe = path6.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
17804
- fs6.writeFileSync(probe, "ok");
17805
- fs6.unlinkSync(probe);
18067
+ const probe = path7.join(dir, `.write-probe.${process.pid}.${(0, import_crypto2.randomBytes)(4).toString("hex")}`);
18068
+ fs7.writeFileSync(probe, "ok");
18069
+ fs7.unlinkSync(probe);
17806
18070
  return true;
17807
18071
  } catch (error) {
17808
18072
  errLine(`Config dir is not writable: ${error instanceof Error ? error.message : String(error)}`);
@@ -18052,7 +18316,7 @@ var bundledPkg = require_package();
18052
18316
  var pkg = (() => {
18053
18317
  try {
18054
18318
  const onDisk = JSON.parse(
18055
- fs10.readFileSync(path10.resolve(__dirname, "..", "package.json"), "utf8")
18319
+ fs11.readFileSync(path11.resolve(__dirname, "..", "package.json"), "utf8")
18056
18320
  );
18057
18321
  if (typeof onDisk.version === "string" && onDisk.version.length > 0) {
18058
18322
  return { version: onDisk.version };
@@ -18064,7 +18328,7 @@ var pkg = (() => {
18064
18328
  dotenv2.config();
18065
18329
  {
18066
18330
  const configDirOverride = process.env.GHL_MCP_CONFIG_DIR?.trim();
18067
- if (configDirOverride && !path10.isAbsolute(configDirOverride)) {
18331
+ if (configDirOverride && !path11.isAbsolute(configDirOverride)) {
18068
18332
  process.stderr.write(
18069
18333
  `[ghl-mcp] GHL_MCP_CONFIG_DIR must be an absolute path (got "${configDirOverride}"). Use e.g. /data/ghl-mcp in a container, with a volume mounted at /data.
18070
18334
  `
@@ -18077,20 +18341,20 @@ process.on("unhandledRejection", (reason) => {
18077
18341
  `);
18078
18342
  });
18079
18343
  function hardenSecretFilePerms() {
18080
- const repoDir = path10.resolve(__dirname, "..");
18344
+ const repoDir = path11.resolve(__dirname, "..");
18081
18345
  const candidates = [
18082
- { file: path10.join(repoDir, "start-mcp.sh"), mode: 448 },
18346
+ { file: path11.join(repoDir, "start-mcp.sh"), mode: 448 },
18083
18347
  // Legacy registry location (pre-migration); new location lives in app-data.
18084
- { file: path10.join(repoDir, ".ghl-tokens.json"), mode: 384 },
18348
+ { file: path11.join(repoDir, ".ghl-tokens.json"), mode: 384 },
18085
18349
  { file: tokenRegistryPath(), mode: 384 }
18086
18350
  ];
18087
18351
  for (const { file, mode } of candidates) {
18088
18352
  let current;
18089
18353
  try {
18090
- if (!fs10.existsSync(file)) continue;
18091
- current = fs10.statSync(file).mode & 511;
18354
+ if (!fs11.existsSync(file)) continue;
18355
+ current = fs11.statSync(file).mode & 511;
18092
18356
  if (current !== mode) {
18093
- fs10.chmodSync(file, mode);
18357
+ fs11.chmodSync(file, mode);
18094
18358
  }
18095
18359
  } catch (error) {
18096
18360
  const message = error instanceof Error ? error.message : String(error);
@@ -18256,6 +18520,9 @@ async function resolveAccessAndRegister() {
18256
18520
  `);
18257
18521
  }
18258
18522
  inBootstrapMode = !apiKey || !locationId || !licenseVerified;
18523
+ const gatingConfig = parseAllowlist(process.env);
18524
+ const outOfBandTracker = makeOutOfBandTracker();
18525
+ outOfBandTracker.registered.add("get_mcp_version");
18259
18526
  if (inBootstrapMode) {
18260
18527
  process.stderr.write(
18261
18528
  `[ghl-mcp] Bootstrap mode.
@@ -18265,13 +18532,52 @@ async function resolveAccessAndRegister() {
18265
18532
  );
18266
18533
  registerSetupTool(server);
18267
18534
  registerLeadCaptureTool(server);
18268
- registerFirebaseCaptureScriptTool(server);
18535
+ registerOutOfBand(
18536
+ [{ name: "auto_capture_firebase_script", register: () => registerFirebaseCaptureScriptTool(server) }],
18537
+ gatingConfig,
18538
+ outOfBandTracker
18539
+ );
18269
18540
  } else {
18270
18541
  const client = new GHLClient({ apiKey, locationId });
18271
- registerAllTools(server, client, registry, pkg.version, process.env, sessionTier);
18272
- registerEnableWorkflowBuilderTool(server);
18273
- registerFirebaseCaptureScriptTool(server);
18274
- registerInteractiveCaptureTool(server);
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}
18570
+ `);
18571
+ } catch (e) {
18572
+ process.stderr.write(`[ghl-mcp] Skills install skipped: ${String(e).slice(0, 120)}
18573
+ `);
18574
+ }
18575
+ }
18576
+ }
18577
+ ],
18578
+ gatingConfig,
18579
+ outOfBandTracker
18580
+ );
18275
18581
  if (fileCreds && !process.env.GHL_API_KEY) {
18276
18582
  process.stderr.write(`[ghl-mcp] Loaded credentials from ${credentialsPath()}
18277
18583
  `);