Skip to main content
Glama
jamersoncalixto

ghl-mcp-remote

README.md
# ghl-mcp-remote

Servidor MCP (Model Context Protocol) remoto para GoHighLevel — **multi-tenant**, acessível
via URL, para ser usado a partir do Claude ou do ChatGPT por qualquer agência, sem que cada
uma precise rodar nada localmente.

Este é um projeto **separado** do [`ghl-mcp`](../ghl-mcp) original (stdio, uso pessoal/local).
Nenhum dos dois depende do outro.

## Diferença para o `ghl-mcp` original

| | `ghl-mcp` (original) | `ghl-mcp-remote` (este) |
|---|---|---|
| Transporte | stdio (processo local) | HTTP (`POST /mcp`), hosteável |
| Tenants | 1 agência por instalação, credenciais em `~/.ghl-mcp/credentials.json` | Qualquer nº de agências, isoladas por `companyId`, credenciais no Postgres |
| "Login" | `npm run auth` no terminal | Tela de autorização da própria GHL, disparada pelo Claude/ChatGPT |
| Uso | Você, localmente | Qualquer empresa, a partir do Claude.ai/ChatGPT, via URL |

O código de negócio (as tools em `src/tools/`) é praticamente idêntico nos dois — só a camada
de autenticação/armazenamento muda.

## Arquitetura

```
Claude/ChatGPT ──(1) descobre──> GET /.well-known/oauth-authorization-server
               ──(2) registra───> POST /register                      (DCR, automático)
               ──(3) pede login─> GET /authorize ──redirect──> tela da GHL (o "login")
                                                        <──redirect── GET /oauth/ghl/callback
               <──code+state───── (nosso próprio código de autorização)
               ──(4) troca──────> POST /token ──> access_token + refresh_token nossos
               ──(5) chama tool─> POST /mcp  (Authorization: Bearer <access_token>)
```

- **"Login" = autorizar a GHL.** Não existe conta/senha própria deste serviço. Quando o
  admin de uma agência aprova o acesso na tela da própria GHL, isso já cria/atualiza o tenant
  dele (identificado pelo `companyId` da GHL) e completa o login do lado do MCP.
- **Um único app GHL Marketplace** (mesmo `GHL_CLIENT_ID`/`GHL_CLIENT_SECRET`) atende
  qualquer agência que o instalar — não é preciso criar um app por cliente.
- Cada chamada de tool chega autenticada com um Bearer token emitido por este servidor;
  o middleware resolve esse token pro `companyId` certo e injeta isso num
  `AsyncLocalStorage` (`src/tenant-context.ts`) — é assim que o código das tools (idêntico
  ao do projeto original) permanece "sem saber" de multi-tenancy.
- Implementado em cima do que o próprio `@modelcontextprotocol/sdk` já traz para servidores
  OAuth (`server/auth/router.ts`, `provider.ts`) — ver `src/auth/mcp-oauth-provider.ts`.

## Pré-requisitos pra rodar em qualquer lugar

1. **App OAuth no GHL Marketplace** (Developer > seu app), distribuição "Agency" ou
   "Agency & Sub-Account":
   - Redirect URI cadastrada: `<PUBLIC_URL>/oauth/ghl/callback` (precisa ser a URL pública
     final deste serviço — HTTPS).
   - Scopes: os mesmos listados em [`src/services/scopes.ts`](src/services/scopes.ts).
2. **Postgres** (qualquer um — Supabase, Neon, RDS, o Postgres gerenciado da própria
   plataforma de hosting, etc.). Rodar [`db/schema.sql`](db/schema.sql) nele uma vez.
3. **Node.js 20+** (ou a imagem Docker deste projeto, que já inclui isso).

## Variáveis de ambiente

Ver [`.env.example`](.env.example). Resumo:

| Variável | Descrição |
|---|---|
| `GHL_CLIENT_ID` / `GHL_CLIENT_SECRET` | Do app OAuth do GHL Marketplace |
| `PUBLIC_URL` | URL pública final deste serviço, sem barra no final |
| `PORT` | Porta em que o processo escuta (muitas plataformas sobrescrevem sozinhas) |
| `DATABASE_URL` | Connection string do Postgres |
| `TOKEN_ENCRYPTION_KEY` | 32 bytes em base64 — `openssl rand -base64 32` |

## Rodar localmente (dev)

```bash
npm install
npm run build
npm start
```

Verificações possíveis sem nenhum domínio público:

```bash
curl localhost:8080/healthz
curl localhost:8080/.well-known/oauth-authorization-server
```

O fluxo completo de OAuth (autorizar de verdade na GHL, ganhar token, chamar uma tool) só
funciona com uma `PUBLIC_URL` real (HTTPS) no ar, porque a GHL precisa conseguir redirecionar
o navegador do admin da agência de volta pra cá — e essa mesma URL precisa estar cadastrada
como redirect URI no app GHL.

## Deploy

Este projeto **não assume nenhuma plataforma de hosting específica** — só inclui um
`Dockerfile` genérico. Qualquer plataforma que rode uma imagem Docker (ou `node dist/index.js`
direto) serve, desde que:

1. Exponha uma URL pública HTTPS estável → isso vira `PUBLIC_URL`.
2. Injete as variáveis de ambiente da tabela acima.
3. O Postgres apontado por `DATABASE_URL` já tenha rodado `db/schema.sql`.
4. O redirect URI do app GHL Marketplace seja atualizado pra `<PUBLIC_URL>/oauth/ghl/callback`
   assim que a URL final for conhecida.

## Conectar no Claude / ChatGPT

Depois de hospedado:

- **Claude.ai / Claude Desktop**: Configurações → Connectors → Add custom connector → URL:
  `https://<seu-dominio>/mcp`. O Claude vai te levar pro fluxo de autorização automaticamente.
- **ChatGPT**: em workspaces com suporte a Connectors/MCP remoto (varia por plano — Team,
  Enterprise, ou "Developer mode"), adicionar um connector apontando pra
  `https://<seu-dominio>/mcp`.

> **Caveat sobre o ChatGPT**: o suporte a conectores MCP remotos com OAuth no ChatGPT varia
> por plano/workspace, e algumas superfícies (ex. Deep Research) restringem quais formatos de
> tool aceitam (às vezes só tools no formato "search"/"fetch"). Este servidor segue a spec de
> autorização do MCP à risca (a mesma que o Claude usa), o que maximiza compatibilidade — mas
> vale testar de verdade assim que estiver hospedado, já que o comportamento do lado do
> ChatGPT foge do nosso controle.

## Estrutura

```
src/
  index.ts                 App Express: monta o router de OAuth, POST/GET/DELETE /mcp,
                            GET /oauth/ghl/callback, GET /healthz, CORS.
  server.ts                 createMcpServer() — registra as tools (idêntico ao projeto original).
  tenant-context.ts          AsyncLocalStorage que carrega o companyId durante cada request.
  db/
    pool.ts                  Pool do `pg` a partir de DATABASE_URL.
    crypto.ts                 AES-256-GCM (tokens da GHL em repouso) + SHA-256 (hash dos nossos tokens).
    agencies.ts                Tokens de agência da GHL por companyId (substitui o antigo token-store.ts).
    oauth-store.ts              Clients MCP, pending auth, authorization codes, access/refresh tokens.
  auth/
    ghl-oauth.ts               Troca/refresh de tokens com a GHL — equivalente ao oauth-flow.ts original,
                               mas web-based e por tenant em vez de CLI + arquivo único.
    location-tokens.ts          Cache de location tokens, agora chaveado por companyId.
    mcp-oauth-provider.ts        Implementa OAuthServerProvider do SDK — o núcleo do "login = autorizar a GHL".
    ghl-callback.ts               Handler de GET /oauth/ghl/callback.
  services/
    constants.ts, scopes.ts, ghl-client.ts   Idênticos ao projeto original (só o import de token mudou).
  tools/
    *.ts                       Idênticos ao projeto original, exceto locations.ts (cache agora por tenant).
db/
  schema.sql                  DDL do Postgres — rodar uma vez antes do primeiro start.
```

## Segurança

- Refresh tokens da GHL: criptografados em repouso (AES-256-GCM).
- Access/refresh tokens que este servidor emite para Claude/ChatGPT: guardados só como hash
  SHA-256 — nunca em texto puro, igual senha.
- PKCE (S256) obrigatório em todo o fluxo MCP-side, validado localmente (não delegado à GHL).
- Nenhuma credencial de uma agência é acessível a partir do token de outra — todo acesso ao
  Postgres é filtrado por `companyId`, e esse valor só entra em cena depois que o Bearer token
  é validado.