In short
- MCP is the open standard for giving AI agents access to external tools and data.
- A GHL MCP server wraps your most-used GHL API calls in typed tool definitions.
- Connect the server to Claude Desktop, Claude API, or any MCP-compatible AI system.
- One MCP server can serve multiple AI workflows and multiple sub-accounts.
Endpoints
| Method | Path | Scopes | Rate limit |
|---|---|---|---|
| JSON-RPC 2.0 | stdio or HTTP (MCP transport) | None (MCP protocol) | Depends on GHL API calls made |
| GET | /contacts/search (called internally) | contacts.readonly | See GHL API docs |
| POST | /contacts (called internally) | contacts.write | See GHL API docs |
| POST | /calendars/events/appointments (called internally) | calendars.write | See GHL API docs |
Authentication
Your MCP server holds the GHL token as an environment variable. The AI client calls the MCP server, and the MCP server calls GHL on behalf of the AI. The AI never sees the GHL token directly.
Use a private integration token (long-lived) for personal or internal tools. Use OAuth 2.0 for multi-tenant tools where each user connects their own GHL account.
The MCP server itself can optionally require authentication from the AI client using bearer tokens or an API key, depending on your transport (stdio is implicitly trusted, HTTP requires its own auth layer).
Store GHL tokens in environment variables, not in the tool definitions or schema. The schema is sent to the AI model and should not contain secrets.
Recipes
Scaffold a GHL MCP server with the official MCP SDK
Use the @modelcontextprotocol/sdk package. Install it from npm. The SDK handles the JSON-RPC 2.0 protocol, tool registration, and transport. You write the GHL API calls that back each tool.
Start with stdio transport for local testing with Claude Desktop. Switch to HTTP transport when you want to deploy the server and connect multiple AI clients.
Package setup
mkdir ghl-mcp-server && cd ghl-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
# Optional but recommended:
npm install -D typescript @types/node tsxsrc/index.ts (minimal GHL MCP server)
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const GHL_TOKEN = process.env.GHL_TOKEN!;
const GHL_LOCATION_ID = process.env.GHL_LOCATION_ID!;
const GHL_BASE = 'https://services.leadconnectorhq.com';
const GHL_VERSION = '2021-07-28';
function ghlHeaders() {
return {
Authorization: `Bearer ${GHL_TOKEN}`,
'Content-Type': 'application/json',
Version: GHL_VERSION,
};
}
const server = new McpServer({
name: 'ghl-mcp-server',
version: '1.0.0',
});
// Tool: search contacts
server.tool(
'ghl_search_contact',
'Search for a GoHighLevel contact by email or phone number.',
{
email: z.string().email().optional(),
phone: z.string().optional(),
},
async ({ email, phone }) => {
const params = new URLSearchParams({ locationId: GHL_LOCATION_ID });
if (email) params.append('email', email);
if (phone) params.append('phone', phone);
const res = await fetch(`${GHL_BASE}/contacts/search?${params}`, {
headers: ghlHeaders(),
});
const data = await res.json();
return {
content: [
{
type: 'text',
text: JSON.stringify(data.contacts ?? [], null, 2),
},
],
};
}
);
// Tool: create contact
server.tool(
'ghl_create_contact',
'Create a new contact in GoHighLevel.',
{
email: z.string().email().optional(),
phone: z.string().optional(),
firstName: z.string().optional(),
lastName: z.string().optional(),
tags: z.array(z.string()).optional(),
},
async (fields) => {
const res = await fetch(`${GHL_BASE}/contacts`, {
method: 'POST',
headers: ghlHeaders(),
body: JSON.stringify({ locationId: GHL_LOCATION_ID, ...fields }),
});
const data = await res.json();
return {
content: [{ type: 'text', text: JSON.stringify(data.contact, null, 2) }],
};
}
);
// Tool: book appointment
server.tool(
'ghl_book_appointment',
'Book an appointment in a GoHighLevel calendar.',
{
calendarId: z.string(),
contactId: z.string(),
startTime: z.string().describe('ISO 8601 UTC datetime'),
endTime: z.string().describe('ISO 8601 UTC datetime'),
title: z.string().optional(),
},
async ({ calendarId, contactId, startTime, endTime, title }) => {
const res = await fetch(`${GHL_BASE}/calendars/events/appointments`, {
method: 'POST',
headers: ghlHeaders(),
body: JSON.stringify({
calendarId,
locationId: GHL_LOCATION_ID,
contactId,
startTime,
endTime,
title: title ?? 'Appointment',
appointmentStatus: 'confirmed',
}),
});
const data = await res.json();
return {
content: [{ type: 'text', text: JSON.stringify(data.appointment, null, 2) }],
};
}
);
// Start the server
const transport = new StdioServerTransport();
await server.connect(transport);Connect the server to Claude Desktop
Add the MCP server to Claude Desktop's config file. On macOS the file is at ~/Library/Application Support/Claude/claude_desktop_config.json. On Windows it is at %APPDATA%\Claude\claude_desktop_config.json.
After saving the config, restart Claude Desktop. The GHL tools appear in the tool picker and Claude can call them in conversation.
claude_desktop_config.json
{
"mcpServers": {
"ghl": {
"command": "node",
"args": ["/absolute/path/to/ghl-mcp-server/src/index.js"],
"env": {
"GHL_TOKEN": "your_private_integration_token",
"GHL_LOCATION_ID": "your_location_id"
}
}
}
}Test with the MCP inspector
# Install the MCP inspector
npm install -g @modelcontextprotocol/inspector
# Run your server through the inspector
GHL_TOKEN=your_token GHL_LOCATION_ID=your_id \
npx @modelcontextprotocol/inspector node src/index.jsDeploy as an HTTP MCP server for multiple clients
Stdio transport only works for one client at a time (Claude Desktop, a local script). For multi-client deployments, switch to Streamable HTTP transport and deploy to any Node.js host.
With HTTP transport, any MCP-compatible client (Claude API, OpenAI, Cursor, Windsurf, and others) can connect by providing the server URL and a client key.
HTTP transport (Streamable HTTP)
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
// ... (same tool definitions as above) ...
const app = express();
app.use(express.json());
// Simple API key check, replace with real auth in production
app.use((req, res, next) => {
if (req.headers['x-api-key'] !== process.env.MCP_API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
app.post('/mcp', async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => console.log('GHL MCP server on port 3000'));Common errors
- Tool not appearing in Claude Desktop: the path in claude_desktop_config.json is wrong or uses a relative path. Always use an absolute path. Check the Claude Desktop developer logs for the exact error.
- GHL API calls failing from MCP server: the environment variables are not being passed to the server process. Confirm they are set in the env block of claude_desktop_config.json and not just in your shell.
- Zod validation error on tool input: the AI is passing a field value that does not match your schema (e.g. a non-UTC datetime). Make the types more permissive or add a description that tells the AI the expected format.
- Rate limit errors: the AI is calling GHL tools in a tight loop. Add a short delay between calls or implement a queue in your server. Log all tool invocations to spot runaway loops.
MCP tool definition
Tool definitions summary (JSON)
[
{
"name": "ghl_search_contact",
"description": "Search for a GoHighLevel contact by email or phone.",
"inputSchema": {
"type": "object",
"properties": {
"email": { "type": "string" },
"phone": { "type": "string" }
}
}
},
{
"name": "ghl_create_contact",
"description": "Create a new contact in GoHighLevel.",
"inputSchema": {
"type": "object",
"properties": {
"email": { "type": "string" },
"phone": { "type": "string" },
"firstName": { "type": "string" },
"lastName": { "type": "string" },
"tags": { "type": "array", "items": { "type": "string" } }
}
}
},
{
"name": "ghl_book_appointment",
"description": "Book an appointment in a GoHighLevel calendar.",
"inputSchema": {
"type": "object",
"properties": {
"calendarId": { "type": "string" },
"contactId": { "type": "string" },
"startTime": { "type": "string" },
"endTime": { "type": "string" }
},
"required": ["calendarId", "contactId", "startTime", "endTime"]
}
}
]