DLF Agency MCP Server
by drleadflow
README.md
# DLF Agency MCP Server
A remote [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that gives AI agents full control over [GoHighLevel](https://www.gohighlevel.com/). 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`
## What It Does
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:
- *"Show me all appointments for this week"*
- *"Create a new contact named John Smith with email john@example.com"*
- *"Search my pipeline for deals over $5,000"*
- *"Create a Conversation AI agent for SMS auto-replies"*
- *"List all invoices from the last 30 days"*
- *"Build a workflow that triggers on form submission"*
## Quick Start (Connecting as a User)
### 1. Get Your API Key
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.
### 2. Connect Your MCP Client
**Claude Code:**
```bash
claude mcp add --transport http \
--header "X-User-Key: uk_YOUR_KEY_HERE" \
dlf-agency https://dlf-agency.skool-203.workers.dev/mcp
```
**Claude Desktop / Cursor / Any MCP Client (`.mcp.json`):**
```json
{
"mcpServers": {
"dlf-agency": {
"type": "http",
"url": "https://dlf-agency.skool-203.workers.dev/mcp",
"headers": {
"X-User-Key": "uk_YOUR_KEY_HERE"
}
}
}
}
```
### 3. Start Using It
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"
```
## Architecture
```
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)
```
### Why KV for Auth?
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.
## All Routes
| 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 |
## Authentication
### User-Key Auth (Primary -- for MCP clients)
Every request to `/mcp` must include an API key:
```
Header: X-User-Key: uk_58894012-805b-4081-89c8-2ad0391e6c2b
```
Keys 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:**
1. User registers at `/signup` -> status = `pending`
2. Admin approves in admin panel -> status = `active`
3. Admin can disable at any time -> status = `disabled`
**Per-user access control:**
- **Scopes**: JSON array of tool names the user can call (e.g., `["ghl_get_contact", "ghl_send_message"]`), or `["*"]` for all 508 tools
- **Allowed Accounts**: JSON array of GHL location IDs the user can access (e.g., `["W7BRJwzJCvFs9r0xZHrE"]`), or `["*"]` for all
### GHL OAuth (for Adding Locations)
To connect a new GHL location (sub-account):
1. Visit `GET /install` -- returns the GHL OAuth chooselocation URL
2. Open that URL in a browser -- select the GHL location to install
3. GHL redirects to `/callback?code=xxx`
4. Server exchanges the code for access + refresh tokens
5. Tokens are stored in D1 with the location ID
6. Tokens auto-refresh when they expire (24h lifetime)
This uses the GHL Marketplace OAuth flow with these credentials:
- **Client ID**: Set via `GHL_CLIENT_ID` secret
- **Client Secret**: Set via `GHL_CLIENT_SECRET` secret
- **Redirect URI**: `https://dlf-agency.skool-203.workers.dev/callback`
### Private Integration Tokens (PITs)
For locations that don't use OAuth, you can manually add them with a GHL Private Integration Token:
1. In GHL: Settings > Integrations > Private Integrations > Create
2. Copy the token
3. Use the `ghl_add_sub_account` tool or the admin panel to add the location with its PIT
PITs don't expire but also can't auto-refresh. OAuth tokens are preferred.
## Admin Panel
**URL:** `https://dlf-agency.skool-203.workers.dev/admin`
Features:
- View, create, edit, and delete users
- Set per-user scopes (which tools they can use)
- Set per-user account access (which GHL locations they can access)
- View and manage sub-accounts
- Scope picker with category presets (Full/Read-Only/None)
Login requires the `ADMIN_PASSWORD` secret.
## Tool Domains (508 Tools)
| 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) |
### Beta: Workflow Builder (16 tools)
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.
### Disabled: Pipeline Write Operations
Three pipeline tools are disabled because GHL returns 401 for all token types:
- `ghl_create_pipeline` -- disabled
- `ghl_update_pipeline` -- disabled
- `ghl_delete_pipeline` -- disabled
Reading 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.
## Sub-Account Management
The server supports multiple GHL locations (sub-accounts). Each has its own API key stored in D1.
**Resolution order** (when a tool is called):
1. If `locationId` is passed in the tool args -> use that location's key from D1
2. If no `locationId` -> use the default account from D1
3. If no default in D1 -> fall back to `GHL_API_KEY` + `GHL_LOCATION_ID` env vars
**Token types:**
- **OAuth tokens**: Have `refresh_token` + `expires_at`. Auto-refresh before expiry.
- **Private Integration tokens**: Static, never expire, no refresh needed.
## Project Structure
```
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 presets
```
### Two-Layer Module Pattern
Every 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 tools
```
**Client 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()`.
## Deployment
### Requirements
- [Cloudflare Workers](https://workers.cloudflare.com/) account
- [Node.js](https://nodejs.org/) 18+
- A GHL account with API access
- GHL OAuth app credentials (for OAuth install flow)
### Setup from Scratch
```bash
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
```
### Secrets Reference
| 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 |
### Cloudflare Bindings
| 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 |
### Deploy Script
```bash
# 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
```
## Security
| 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` |
## GHL API Versions
| 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.
## Adding New Tools
### Add a tool to an existing domain
```bash
# 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
```
### Add a new domain
```bash
# 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.ts
```
## D1 Database Schema
### users
```sql
id 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 TEXT
```
### sub_accounts
```sql
id 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
```
## Costs
| 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.
## License
[Apache 2.0](LICENSE)
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessSyncing