Dlf Ghl Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Dlf Ghl Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A remote Model Context Protocol (MCP) server that gives AI agents full control over GoHighLevel. Deployed as a Cloudflare Worker with user-key authentication, per-user scopes, multi-account support, and an admin panel.
508 tools across 18 domain modules covering the entire GHL API v2 surface + internal workflow builder API.
Live at: https://dlf-agency.skool-203.workers.dev
Connect any MCP-compatible AI client (Claude Code, Claude Desktop, Cursor, Windsurf, custom agents) to your GoHighLevel account. The AI can then manage your entire GHL workspace through natural language:
Go to https://dlf-agency.skool-203.workers.dev/signup and register. Your key (uk_...) is shown once -- copy it immediately. An admin must approve your account before it works.
Claude Code:
claude mcp add --transport http \
--header "X-User-Key: uk_YOUR_KEY_HERE" \
dlf-agency https://dlf-agency.skool-203.workers.dev/mcpClaude Desktop / Cursor / Any MCP Client (`.mcp.json`):
{
"mcpServers": {
"dlf-agency": {
"type": "http",
"url": "https://dlf-agency.skool-203.workers.dev/mcp",
"headers": {
"X-User-Key": "uk_YOUR_KEY_HERE"
}
}
}
}Once connected, ask your AI to do anything in GHL:
"List all contacts tagged 'hot-lead'"
"Send an SMS to contact ID xyz saying 'Hey, following up on our call'"
"Create a calendar event for tomorrow at 2pm"MCP Client (Claude Code, Cursor, etc.)
│
│ HTTPS + X-User-Key header
▼
Cloudflare Worker (outer wrapper)
│
├── Validates API key against D1 (SHA-256 hashed)
├── Stores auth context in KV (scopes + allowed accounts)
├── Passes user ID via URL query param
│
▼
GHLMcpAgent (Durable Object)
│
├── Reads auth from KV (scopes, allowed accounts)
├── Enforces per-tool scope checks (default-deny)
├── resolveClient() → picks correct GHL API key for the location
│
▼
GoHighLevel REST API (services.leadconnectorhq.com)The MCP SDK's McpAgent.serve() internally creates a WebSocket upgrade request that strips all custom headers. Only x-partykit-room (session ID) and Upgrade survive. So we store auth context in KV keyed by user ID, and the Durable Object reads it from there.
| Route | Method | Auth | Description |
|---|---|---|---|
/health | GET | None | Health check -- returns server name + version |
/mcp | POST | X-User-Key header | MCP endpoint -- all tool calls go here |
/signup | GET/POST | None (rate limited) | Self-service user registration |
/admin | GET | Password | Admin panel -- manage users, accounts, scopes |
/install | GET | None | Returns GHL OAuth install URL for adding locations |
/callback | GET | None | OAuth callback -- exchanges auth code for tokens |
/refresh | POST | X-Admin-Pin header | Force-refresh all OAuth location tokens |
/admin/agency-token | GET/POST | X-Admin-Pin header | View/store agency PIV token |
/authorize | GET | PIN, session, or user key | OAuth auto-approve |
/register | POST | PIN or user key | OAuth client registration |
Every request to /mcp must include an API key:
Header: X-User-Key: uk_58894012-805b-4081-89c8-2ad0391e6c2bKeys are generated at /signup or by the admin. They are SHA-256 hashed before storage -- the raw key is shown once and can never be recovered.
User lifecycle:
/signup -> status = pendingactivedisabledPer-user access control:
["ghl_get_contact", "ghl_send_message"]), or ["*"] for all 508 tools["W7BRJwzJCvFs9r0xZHrE"]), or ["*"] for allTo connect a new GHL location (sub-account):
GET /install -- returns the GHL OAuth chooselocation URL/callback?code=xxxThis uses the GHL Marketplace OAuth flow with these credentials:
GHL_CLIENT_ID secretGHL_CLIENT_SECRET secrethttps://dlf-agency.skool-203.workers.dev/callbackFor locations that don't use OAuth, you can manually add them with a GHL Private Integration Token:
ghl_add_sub_account tool or the admin panel to add the location with its PITPITs don't expire but also can't auto-refresh. OAuth tokens are preferred.
URL: https://dlf-agency.skool-203.workers.dev/admin
Features:
Login requires the ADMIN_PASSWORD secret.
| Domain | Tools | File | What It Covers |
|---|---|---|---|
| accounts | 5 | accounts.ts | Sub-account management (add, list, switch, remove) |
| ai-agents | 26 | ai-agents.ts | Voice AI, Conversation AI, Agent Studio, call logs |
| automation | 11 | automation.ts | Workflows, forms, surveys |
| businesses | 5 | businesses.ts | Business CRUD |
| calendars | 56 | calendars.ts | Calendars, appointments, groups, resources, services, bookings |
| contacts | 27 | contacts.ts | Contacts, notes, tasks, tags, followers, merge |
| content | 41 | content.ts | Blogs, media, documents, menus, snapshots, templates |
| conversations | 22 | conversations.ts | Messages, calls, transcriptions, attachments |
| errors | 2 | errors.ts | View and clear server error logs |
| knowledge-base | 14 | knowledge-base.ts | Knowledge bases, FAQs, web crawlers |
| locations | 45 | locations.ts | Locations, users, custom fields, tags, business profiles |
| marketing | 70 | marketing.ts | Social media, email campaigns, funnels, links, queues |
| marketplace | 9 | marketplace.ts | App installations, billing, rebilling |
| misc | 66 | misc.ts | Companies, phone numbers, products, custom objects, brands |
| opportunities | 12 | opportunities.ts | Deals, pipelines, followers |
| payments | 68 | payments.ts | Invoices, orders, subscriptions, estimates, coupons, shipping |
| saas | 13 | saas.ts | SaaS rebilling, subscriptions, agency plans, wallets |
| workflow-builder | 16 | workflow-builder.ts | Workflow CRUD, triggers, steps, publish/draft (BETA) |
The workflow builder tools use an internal GHL API (backend.leadconnectorhq.com) with Firebase authentication. These are not part of the official GHL REST API and may change without notice.
Tools: ghl_workflow_builder_list, ghl_workflow_builder_create, ghl_workflow_builder_get, ghl_workflow_builder_get_steps, ghl_workflow_builder_get_triggers, ghl_workflow_builder_update, ghl_workflow_builder_save_steps, ghl_workflow_builder_publish, ghl_workflow_builder_draft, ghl_workflow_builder_delete, ghl_workflow_builder_create_trigger, ghl_workflow_builder_update_trigger, ghl_workflow_builder_delete_trigger, ghl_workflow_builder_create_folder, ghl_workflow_builder_clone, ghl_workflow_builder_error_count
Status: Beta. These tools may not work reliably for create/update operations. Read operations (list, get) are stable.
Requires GHL_FIREBASE_REFRESH_TOKEN secret to be set.
Three pipeline tools are disabled because GHL returns 401 for all token types:
ghl_create_pipeline -- disabledghl_update_pipeline -- disabledghl_delete_pipeline -- disabledReading pipelines (ghl_list_pipelines, ghl_get_pipeline) works fine. The write operations require a separate opportunities.pipeline.write scope that GHL doesn't currently expose in Private Integration or OAuth token grants.
Code is preserved in src/tools/_disabled/pipeline-write.ts and can be re-enabled when GHL fixes this.
The server supports multiple GHL locations (sub-accounts). Each has its own API key stored in D1.
Resolution order (when a tool is called):
locationId is passed in the tool args -> use that location's key from D1locationId -> use the default account from D1GHL_API_KEY + GHL_LOCATION_ID env varsToken types:
refresh_token + expires_at. Auto-refresh before expiry.dlf-ghl-mcp-server/
├── src/
│ ├── index.ts # Worker entry: auth wrapper + GHLMcpAgent DO
│ ├── types.ts # Env, User, SubAccount, ApiVersion types
│ ├── config.ts # API base URL, versions, MCP server metadata
│ │
│ ├── client/ # GHL API client layer (makes HTTP calls)
│ │ ├── base.ts # BaseGHLClient -- fetch wrapper with auth headers
│ │ ├── index.ts # GHLClient -- composes all 16 domain factories
│ │ ├── ai-agents.ts # Voice AI, Conversation AI, Agent Studio
│ │ ├── automation.ts # Workflows, forms, surveys
│ │ ├── businesses.ts # Business CRUD
│ │ ├── calendars.ts # Calendars, events, bookings, services
│ │ ├── contacts.ts # Contacts, notes, tasks, tags
│ │ ├── content.ts # Blogs, media, documents, menus, snapshots
│ │ ├── conversations.ts # Messages, calls, transcriptions
│ │ ├── knowledge-base.ts # KBs, FAQs, crawlers
│ │ ├── locations.ts # Locations, users, custom fields
│ │ ├── marketing.ts # Social, email, campaigns, funnels, links
│ │ ├── marketplace.ts # Billing, app installations
│ │ ├── misc.ts # Companies, phone, products, objects, brands
│ │ ├── opportunities.ts # Opportunities, pipelines
│ │ ├── payments.ts # Invoices, orders, subscriptions, coupons
│ │ ├── saas.ts # SaaS rebilling, wallets
│ │ └── workflow-builder.ts # Internal GHL API (Firebase auth) -- BETA
│ │
│ ├── tools/ # MCP tool registrations (Zod schemas + handlers)
│ │ ├── index.ts # registerAllTools() -- calls all 18 domain modules
│ │ ├── _helpers.ts # ok(), err(), resolveClient() shared utilities
│ │ ├── _disabled/
│ │ │ └── pipeline-write.ts # Pipeline CRUD (disabled -- GHL returns 401)
│ │ ├── accounts.ts # 5 tools
│ │ ├── ai-agents.ts # 26 tools
│ │ ├── automation.ts # 11 tools
│ │ ├── businesses.ts # 5 tools
│ │ ├── calendars.ts # 56 tools
│ │ ├── contacts.ts # 27 tools
│ │ ├── content.ts # 41 tools
│ │ ├── conversations.ts # 22 tools
│ │ ├── errors.ts # 2 tools
│ │ ├── knowledge-base.ts # 14 tools
│ │ ├── locations.ts # 45 tools
│ │ ├── marketing.ts # 70 tools
│ │ ├── marketplace.ts # 9 tools
│ │ ├── misc.ts # 66 tools
│ │ ├── opportunities.ts # 12 tools
│ │ ├── payments.ts # 68 tools
│ │ ├── saas.ts # 13 tools
│ │ └── workflow-builder.ts # 16 tools (BETA)
│ │
│ ├── db/
│ │ ├── accounts.ts # D1: sub_accounts + oauth_tokens tables
│ │ ├── users.ts # D1: users table, API key hashing
│ │ └── errors.ts # D1: error capture table
│ │
│ ├── handlers/
│ │ ├── admin.ts # Admin panel (HTML dashboard + REST API)
│ │ ├── oauth-callback.ts # GHL OAuth code exchange + token storage
│ │ └── register.ts # User self-registration form
│ │
│ └── utils/
│ ├── errors.ts # GHLError class (statusCode + details)
│ ├── logger.ts # Structured JSON logger with field redaction
│ ├── rate-limit.ts # KV-based sliding window rate limiter
│ └── webhook.ts # Optional error webhook sender
│
├── scripts/
│ ├── deploy.sh # Full deploy pipeline (both workers)
│ ├── check-duplicates.sh # Detect duplicate tool names (crash prevention)
│ ├── count-tools.sh # Tool count per domain (--detail for names)
│ ├── add-domain.sh # Scaffold new domain module pair
│ └── add-tool.sh # Add tool to existing domain
│
├── migrations/
│ ├── 0001_create_users_table.sql
│ └── 0002_add_allowed_accounts.sql
│
├── wrangler.toml # Cloudflare Worker config (bindings, DO, D1, KV)
├── tsconfig.json
├── package.json
└── SERVER-MAP.md # Quick reference with all tool names + scope presetsEvery GHL API domain has a parallel pair of files:
src/client/<domain>.ts -> Factory function returning async methods (HTTP calls)
src/tools/<domain>.ts -> registerXxxTools(server, env) registering MCP toolsClient layer: Each file exports a domainMethods(client) factory. GHLClient composes all 16 factories in its constructor.
Tools layer: Each file exports a registerXxxTools(server, env) function. registerAllTools() calls all 18 registration functions during GHLMcpAgent.init().
git clone https://github.com/Bladefitness/dlf-ghl-mcp.git
cd dlf-ghl-mcp/dlf-ghl-mcp-server
npm install
# Create Cloudflare resources
npx wrangler d1 create ghl-accounts
npx wrangler kv namespace create OAUTH_KV
# Update wrangler.toml with the IDs from above
# Set required secrets
echo "your-admin-password" | npx wrangler secret put ADMIN_PASSWORD
echo "your-admin-pin" | npx wrangler secret put ADMIN_PIN
echo "your-ghl-client-id" | npx wrangler secret put GHL_CLIENT_ID
echo "your-ghl-client-secret" | npx wrangler secret put GHL_CLIENT_SECRET
# Optional: for workflow builder tools
echo "your-firebase-refresh-token" | npx wrangler secret put GHL_FIREBASE_REFRESH_TOKEN
# Deploy
npm run deploy
# Verify
curl https://your-worker.workers.dev/health| Secret | Required | Purpose |
|---|---|---|
ADMIN_PASSWORD | Yes | Admin panel login password |
ADMIN_PIN | Yes | X-Admin-Pin header for API admin routes |
GHL_CLIENT_ID | Yes | GHL OAuth app Client ID |
GHL_CLIENT_SECRET | Yes | GHL OAuth app Client Secret |
GHL_FIREBASE_REFRESH_TOKEN | No | Firebase refresh token (workflow builder BETA) |
GHL_FIREBASE_TOKEN | No | Static Firebase ID token (fallback) |
ERROR_WEBHOOK_URL | No | Webhook URL for error reports |
| Binding | Type | Purpose |
|---|---|---|
MCP_OBJECT | Durable Object | MCP session persistence (GHLMcpAgent) |
GHL_DB | D1 Database | Users, sub-accounts, OAuth tokens, errors |
OAUTH_KV | KV Namespace | Auth context, sessions, rate limit counters |
# Full pipeline: duplicate check -> tsc -> deploy both workers -> health verify
./scripts/deploy.sh
# Dry run (no actual deploy)
./scripts/deploy.sh --dry-run
# Deploy only one worker
./scripts/deploy.sh --one dlf-agency| Protection | How |
|---|---|
| API key hashing | SHA-256 hash stored in D1 -- raw key never persisted |
| Per-user scopes | Default-deny: no scopes = no tools. Admin must grant access |
| Account isolation | Users can only access GHL locations in their allowed_accounts |
| Rate limiting | 120 req/min on /mcp, 5 req/min on /signup |
| Header sanitization | Incoming X-User-Scopes, X-User-Allowed-Accounts stripped to prevent spoofing |
| Timing-safe auth | Admin PIN uses HMAC-SHA256 constant-time comparison |
| Session fingerprinting | Admin sessions bound to IP + User-Agent |
| Error redaction | API keys, tokens, passwords masked in all error logs |
| CORS | Admin routes restricted to same-origin; public routes use wildcard |
| Security headers | X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer |
| Version | Used For |
|---|---|
2021-07-28 | Most endpoints (contacts, conversations, invoices, workflows, etc.) |
2021-04-15 | Calendar events, blocked slots, Conversation AI agents, calls, transcriptions |
The correct version is set automatically per endpoint in each client module.
# Scaffold both client method + tool registration
./scripts/add-tool.sh contacts ghl_archive_contact
# Or manually:
# 1. Add method to src/client/contacts.ts
# 2. Add server.tool() to src/tools/contacts.ts
# 3. Run ./scripts/check-duplicates.sh to verify no name collision# Scaffold the full module pair
./scripts/add-domain.sh new-domain
# Then wire it up:
# 1. Import factory in src/client/index.ts
# 2. Import register function in src/tools/index.tsid TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
api_key TEXT NOT NULL UNIQUE, -- SHA-256 hash of uk_<uuid>
status TEXT DEFAULT 'pending', -- 'pending' | 'active' | 'disabled'
scopes TEXT DEFAULT '["*"]', -- JSON array of tool names or ["*"]
allowed_accounts TEXT DEFAULT '["*"]', -- JSON array of location IDs or ["*"]
created_at TEXT, updated_at TEXT, notes TEXTid TEXT PRIMARY KEY, -- GHL Location ID
name TEXT NOT NULL,
api_key TEXT NOT NULL, -- Bearer token (PIT or OAuth)
account_type TEXT DEFAULT 'sub_account', -- 'sub_account' | 'oauth_location'
is_default INTEGER DEFAULT 0,
refresh_token TEXT, -- OAuth only
expires_at INTEGER, -- Unix timestamp, OAuth only
notes TEXT, created_at TEXT, updated_at TEXT| Resource | Free Tier | Paid |
|---|---|---|
| Workers requests | 100K/day | $0.30/M |
| Durable Objects | -- | $0.15/M requests |
| D1 reads | 5M/day | $0.001/M rows |
| KV reads | 100K/day | $0.50/M reads |
For personal/small team use, this typically stays within free tier limits.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.