@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.
- package/CHANGELOG.md +45 -0
- package/README.md +40 -6
- package/dist/index.js +485 -179
- package/package.json +4 -3
- package/skills/blueprint/README.md +27 -0
- package/skills/blueprint/SKILL.md +149 -0
- package/skills/blueprint/examples/medspa-approval-view.md +92 -0
- package/skills/blueprint/examples/medspa-brief.json +52 -0
- package/skills/blueprint/examples/medspa-build-plan.json +265 -0
- package/skills/blueprint/examples/medspa-dry-run-report.md +67 -0
- package/skills/blueprint/examples/sample-approval-view.md +82 -0
- package/skills/blueprint/examples/sample-brief.json +13 -0
- package/skills/blueprint/examples/sample-build-plan.json +227 -0
- package/skills/blueprint/examples/validate-plan.cjs +127 -0
- package/skills/blueprint/presets/clinic-launch-a2p.md +39 -0
- package/skills/blueprint/presets/clinic-launch-a2p.preset.json +337 -0
- package/skills/blueprint/presets/generic-client.md +36 -0
- package/skills/blueprint/presets/generic-client.preset.json +255 -0
- package/skills/blueprint/presets/med-spa.md +59 -0
- package/skills/blueprint/presets/med-spa.preset.json +264 -0
- package/skills/blueprint/references/agency-os-detection.md +80 -0
- package/skills/blueprint/references/approval-view.md +83 -0
- package/skills/blueprint/references/brief-schema.md +45 -0
- package/skills/blueprint/references/build-plan-schema.md +52 -0
- package/skills/blueprint/references/external-funnel.md +310 -0
- package/skills/blueprint/references/intake-question-set.md +141 -0
- 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(
|
|
118
|
-
const url = new 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,
|
|
129
|
-
const url = this.buildUrl(
|
|
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} ${
|
|
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} ${
|
|
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,
|
|
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} ${
|
|
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,
|
|
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} ${
|
|
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(
|
|
187
|
-
return this.request("GET",
|
|
186
|
+
async get(path12, options) {
|
|
187
|
+
return this.request("GET", path12, options);
|
|
188
188
|
}
|
|
189
|
-
async post(
|
|
190
|
-
return this.request("POST",
|
|
189
|
+
async post(path12, options) {
|
|
190
|
+
return this.request("POST", path12, options);
|
|
191
191
|
}
|
|
192
|
-
async put(
|
|
193
|
-
return this.request("PUT",
|
|
192
|
+
async put(path12, options) {
|
|
193
|
+
return this.request("PUT", path12, options);
|
|
194
194
|
}
|
|
195
|
-
async patch(
|
|
196
|
-
return this.request("PATCH",
|
|
195
|
+
async patch(path12, options) {
|
|
196
|
+
return this.request("PATCH", path12, options);
|
|
197
197
|
}
|
|
198
|
-
async delete(
|
|
199
|
-
return this.request("DELETE",
|
|
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
|
|
1001
|
+
const os6 = require("node:os");
|
|
1002
1002
|
const crypto4 = require("node:crypto");
|
|
1003
|
-
const raw = `${
|
|
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
|
|
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 ? "
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
2105
|
+
version: "3.56.0",
|
|
2106
2106
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
2107
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
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
|
|
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
|
-
|
|
2203
|
+
fs8.accessSync(systemApps, fs8.constants.W_OK);
|
|
2203
2204
|
base = "";
|
|
2204
2205
|
} catch {
|
|
2205
2206
|
}
|
|
2206
|
-
const appDir = base ?
|
|
2207
|
-
const macOSDir =
|
|
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:
|
|
2237
|
-
{ path:
|
|
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 =
|
|
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 =
|
|
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,
|
|
2280
|
+
const plan = planLauncher(process.platform, os4.homedir(), port, installingEntry());
|
|
2280
2281
|
try {
|
|
2281
2282
|
for (const f of plan.files) {
|
|
2282
|
-
|
|
2283
|
-
|
|
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
|
|
2304
|
+
var fs8, os4, path8;
|
|
2304
2305
|
var init_launcher = __esm({
|
|
2305
2306
|
"src/launcher.ts"() {
|
|
2306
2307
|
"use strict";
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
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 =
|
|
2339
|
-
const fromPath = (process.env.PATH || "").split(
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2458
|
+
resolve6(outcome);
|
|
2458
2459
|
});
|
|
2459
2460
|
});
|
|
2460
2461
|
}
|
|
2461
|
-
var import_child_process2,
|
|
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
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
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
|
|
3189
|
+
return path10.join(appDataDir(), "intake-overlay.json");
|
|
3189
3190
|
}
|
|
3190
3191
|
function readOverlay() {
|
|
3191
3192
|
try {
|
|
3192
|
-
const raw = JSON.parse(
|
|
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
|
-
|
|
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
|
|
3213
|
+
return path10.join(appDataDir(), "cockpit-state.json");
|
|
3213
3214
|
}
|
|
3214
3215
|
function readCockpitState() {
|
|
3215
3216
|
try {
|
|
3216
|
-
const raw = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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", () =>
|
|
4024
|
-
ls.on("error", () =>
|
|
4024
|
+
ls.on("close", () => resolve6(recorderAllowPatterns(buf)));
|
|
4025
|
+
ls.on("error", () => resolve6([]));
|
|
4025
4026
|
setTimeout(() => {
|
|
4026
4027
|
ls.kill("SIGKILL");
|
|
4027
|
-
|
|
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((
|
|
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", () =>
|
|
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((
|
|
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", () =>
|
|
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((
|
|
4358
|
+
await new Promise((resolve6, reject) => {
|
|
4358
4359
|
server2.once("error", reject);
|
|
4359
|
-
server2.listen(port, "127.0.0.1",
|
|
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((
|
|
4386
|
-
const stop = () => server2.close(() =>
|
|
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
|
|
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
|
-
|
|
4396
|
-
|
|
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
|
|
4432
|
-
var
|
|
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 (!
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
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
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
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
|
|
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
|
|
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,
|
|
8446
|
+
async function builderRequest(method, path12, body) {
|
|
8380
8447
|
const headers = await client.buildHeaders();
|
|
8381
|
-
const response = await fetch(`${EMAIL_BUILDER_BASE}${
|
|
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${
|
|
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(
|
|
9646
|
-
return client.request("GET",
|
|
9712
|
+
async function internalGet(path12) {
|
|
9713
|
+
return client.request("GET", path12);
|
|
9647
9714
|
}
|
|
9648
|
-
async function internalPost(
|
|
9649
|
-
return client.request("POST",
|
|
9715
|
+
async function internalPost(path12, body) {
|
|
9716
|
+
return client.request("POST", path12, body);
|
|
9650
9717
|
}
|
|
9651
|
-
async function internalPut(
|
|
9652
|
-
return client.request("PUT",
|
|
9718
|
+
async function internalPut(path12, body) {
|
|
9719
|
+
return client.request("PUT", path12, body);
|
|
9653
9720
|
}
|
|
9654
|
-
async function internalDelete(
|
|
9655
|
-
return client.request("DELETE",
|
|
9721
|
+
async function internalDelete(path12) {
|
|
9722
|
+
return client.request("DELETE", path12);
|
|
9656
9723
|
}
|
|
9657
|
-
async function funnelRequest(method,
|
|
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${
|
|
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} ${
|
|
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,
|
|
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${
|
|
10619
|
-
if (!response.ok) throw new Error(`Funnel API Error ${response.status}: ${method} ${
|
|
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,
|
|
11005
|
+
async function formApiRequest(client, method, path12, body) {
|
|
10939
11006
|
const headers = await client.buildHeaders();
|
|
10940
|
-
const url = `https://backend.leadconnectorhq.com/forms${
|
|
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} ${
|
|
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,
|
|
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
|
|
11084
|
-
if (formId)
|
|
11085
|
-
if (skip)
|
|
11086
|
-
const result = await formRequest("GET",
|
|
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,
|
|
11546
|
+
async function pipelineRequest(method, path12, body) {
|
|
11480
11547
|
const headers = await client.buildHeaders();
|
|
11481
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
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} ${
|
|
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((
|
|
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((
|
|
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,
|
|
12747
|
+
async function smartListRequest(method, path12, body) {
|
|
12681
12748
|
const headers = await client.buildHeaders();
|
|
12682
|
-
const url = `${SMARTLIST_BASE}${
|
|
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} ${
|
|
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,
|
|
12889
|
+
async function reputationRequest(method, path12) {
|
|
12823
12890
|
const headers = await client.buildHeaders();
|
|
12824
|
-
const response = await fetch(`${REPUTATION_BASE}${
|
|
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} ${
|
|
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(
|
|
13009
|
+
async function membershipRequest(path12, method = "GET", body) {
|
|
12943
13010
|
const headers = await client.buildHeaders();
|
|
12944
|
-
const response = await fetch(`${MEMBERSHIP_BASE}${
|
|
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} ${
|
|
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((
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
14992
|
+
function setPath(target, path12, value) {
|
|
14901
14993
|
if (value === void 0) return;
|
|
14902
|
-
const parts =
|
|
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,
|
|
17080
|
+
const pipelineApi = async (method, path12, body) => {
|
|
16989
17081
|
const headers = await builderClient.buildHeaders();
|
|
16990
|
-
const url = `https://backend.leadconnectorhq.com/opportunities/pipelines${
|
|
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} ${
|
|
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,
|
|
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${
|
|
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} ${
|
|
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 (
|
|
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
|
|
17760
|
-
var
|
|
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 =
|
|
17804
|
-
|
|
17805
|
-
|
|
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
|
-
|
|
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 && !
|
|
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 =
|
|
18344
|
+
const repoDir = path11.resolve(__dirname, "..");
|
|
18081
18345
|
const candidates = [
|
|
18082
|
-
{ file:
|
|
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:
|
|
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 (!
|
|
18091
|
-
current =
|
|
18354
|
+
if (!fs11.existsSync(file)) continue;
|
|
18355
|
+
current = fs11.statSync(file).mode & 511;
|
|
18092
18356
|
if (current !== mode) {
|
|
18093
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18273
|
-
|
|
18274
|
-
|
|
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
|
`);
|