opengym-conector
by derikkruger
README.md
# opengym-conector
Conector MCP remoto para o [openGym](https://github.com/DuarteSantos8/openGym). Cada pessoa da
instância gera um **link pessoal** e cola no Claude (conector personalizado) ou no ChatGPT (modo
desenvolvedor). A partir daí dá para planejar treinos, registrar o que fez e analisar o progresso
conversando, e tudo é gravado pela **API oficial** do openGym, então aparece em todos os aparelhos
da pessoa como se tivesse sido feito no app.
## Licença e código do openGym
Este projeto é **AGPL-3.0-or-later** (texto em [LICENSE](LICENSE)), porque reaproveita código do
openGym, que é AGPL-3.0:
- upstream: <https://github.com/DuarteSantos8/openGym>, tag **v1.4.1**, commit `464355ef43d4c8f8a4e0bb6db9c2af7f240593b5`;
- o código do openGym **não está copiado neste repositório**: `scripts/baixar-opengym.mjs` faz um
clone esparso da tag (pastas `mcp`, `frontend/src/lib`, `frontend/src/exercise-names`), confere
o hash do commit e grava em `./upstream`, no build da imagem ou antes dos testes;
- **um único arquivo do openGym é substituído**: `mcp/src/state.js`, pela versão em
[src/upstream-patch/state.js](src/upstream-patch/state.js). O original lê `./data/state-<uid>.json`
do disco; o nosso recebe o estado lido da API a cada requisição (AsyncLocalStorage) e mantém
todos os exports e o mesmo `defaultsShape()`. Nenhum outro arquivo do upstream é alterado;
- tudo que é usado do upstream está listado em [src/upstream.js](src/upstream.js).
Pela seção 13 da AGPL, quem usa o conector pela rede tem de poder obter o código-fonte. Publique
este repositório e defina `SOURCE_URL`: as páginas passam a mostrar o link "código-fonte".
## Variáveis de ambiente
| Variável | Padrão | Para quê |
|---|---|---|
| `OPENGYM_API` | obrigatória | URL **interna** da API do openGym, por exemplo `http://api:3000` na rede do compose |
| `PUBLIC_URL` | obrigatória | URL pública do conector, por exemplo `https://ia.gym.bruxo.app`; é o começo do link pessoal |
| `PORT` | `8787` | porta HTTP do container (o TLS fica no Traefik) |
| `DATA_DIR` | `/data` | onde fica `conexoes.json` (monte um volume) |
| `TZ` | `America/Sao_Paulo` na imagem | define o "hoje" de treinos e pesagens, como o relógio do aparelho |
| `SOURCE_URL` | vazio | link do código-fonte exibido nas páginas (AGPL) |
## Como um amigo conecta
1. No navegador, entra no site do openGym com a conta dele.
2. **Configurações › Conta** (a linha com o nome dele) **› Parear o aplicativo**. Aparece um código
de 8 letras, que vale 5 minutos e só pode ser usado uma vez. (Rótulos conferidos no pacote pt-BR
do openGym v1.4.1: `Settings` = Configurações, `Account` = Conta, `Pair the mobile app` =
Parear o aplicativo. O item só aparece no site, não dentro do app de celular pareado.)
3. Abre `PUBLIC_URL/conectar`, cola o código e recebe o link pessoal `PUBLIC_URL/mcp/<chave>`.
4. **Claude**: Configurações › Conectores › Adicionar conector personalizado (Add custom connector),
cola o link; na conversa, ativa no botão **+**.
**ChatGPT**: Configurações › Apps › Configurações avançadas › Modo desenvolvedor; cria um
conector com o link e autenticação "Sem autenticação" (No authentication).
Para cancelar: **Sair de todos os dispositivos** no openGym invalida o token, e o conector passa
a responder com erro pedindo para conectar de novo.
## Ferramentas
Leitura (as nove do MCP oficial do openGym, sem alteração, todas com `readOnlyHint: true`):
`list_routines`, `get_routine`, `preview_session`, `get_week_plan`, `list_workouts`,
`get_workout`, `get_bodyweight`, `estimate_1rm`, `muscle_balance`.
Novas:
| Ferramenta | O que faz | Anotações |
|---|---|---|
| `search_exercises` | busca na biblioteca do app (~5600) e nos personalizados, por nome em inglês ou pt-BR, músculo, equipamento | leitura |
| `create_routine` | cria rotina (séries, reps, faixa, carga em kg, descanso, regra de progressão); recusa nome repetido | escrita |
| `update_routine` | renomeia, troca a lista de exercícios (mantendo nota, aquecimento e progressão de quem continua) e a progressão | escrita, `destructiveHint` |
| `delete_routine` | apaga a rotina e tira dos dias da semana, como o app | escrita, `destructiveHint` |
| `set_week_schedule` | põe rotinas nos dias da semana fixa; em modo rodízio recusa e explica, a menos que `switch_to_fixed_week` | escrita, idempotente |
| `log_workout` | registra treino já feito pelo caminho "Registrar um treino anterior" do app; repetição idêntica não duplica | escrita, idempotente |
| `log_bodyweight` | pesagem do dia; dia que já tem pesagem é corrigido | escrita, idempotente |
Pesos entram em kg e são convertidos com as funções do app quando o perfil está em lb.
## Como a sincronização é respeitada
O openGym mescla aparelhos campo a campo, com carimbo por mudança e registro de remoções
(`deleted`, `edited`, `_f`, `_ts`; `frontend/src/lib/sync-merge.js`). Cada gravação do conector
([src/sincronizar.js](src/sincronizar.js)) faz o que o `update()` e o `doPush()` do app fazem
(`frontend/src/store/useStore.js`):
1. `GET /api/data` (documento, `rev` e `_wid`), sem cache entre chamadas;
2. aplica a operação numa cópia, com as funções do próprio app (`deleteRoutine`, `chooseFixedWeek`,
`buildCombinedEntries`, `buildCompletedWorkout`, `completeBackfill`, `rebuildPrHistory`,
`refillAfter`, ...);
3. carimba com `stampChange(anterior, novo)` do app e põe o tempo em `_ts`;
4. `PUT /api/data` com `baseRev`, `baseWid` e `stamped: true`;
5. no 409 a API devolve o documento atual e a operação é reaplicada sobre ele, até 3 vezes.
Gravações do mesmo perfil passam por uma fila, então o 409 só vem de outro aparelho.
Os testes provam com o `mergeStates` do app que um aparelho com cópia velha mantém as gravações do
conector e que nada apagado (por ele ou pelo conector) volta.
## Segurança
- A chave pessoal são 32 bytes aleatórios (base64url) e só existe no link. No disco,
`conexoes.json` guarda `sha256(chave)` para achar o registro e o token do openGym cifrado com
AES-256-GCM, numa chave derivada por HKDF da própria chave pessoal. O arquivo vazado sozinho não
abre nenhum token.
- O link funciona como senha (é um bearer na URL). Quem tiver o link lê e altera os treinos daquela
pessoa, e só dela. A página avisa isso.
- Nunca vão para o log: chave, token, código de pareamento, corpo de requisição. **Atenção ao
Traefik**: se o access log estiver ligado, ele grava o caminho `/mcp/<chave>`. Desligue o access
log desse roteador ou filtre o caminho.
- Limites: 60 pedidos MCP por minuto por link, 10 pareamentos por minuto por endereço
(último item do `X-Forwarded-For`), corpo MCP até 1 MB.
- O token do openGym vale 90 dias (`SESSION_DAYS`); passados ~40, o conector consulta
`GET /api/me` no máximo uma vez por hora e guarda o token novo que a API devolve depois de 45.
- "Sair de todos os dispositivos" no openGym encerra todos os links da pessoa.
## Rodando junto do openGym (compose)
O conector precisa estar na mesma rede Docker do serviço `api` do openGym (porta 3000):
```yaml
services:
conector:
build: ./opengym-conector
restart: unless-stopped
depends_on:
- api
environment:
OPENGYM_API: http://api:3000
PUBLIC_URL: https://ia.gym.bruxo.app
DATA_DIR: /data
PORT: "8787"
TZ: America/Sao_Paulo
# SOURCE_URL: https://endereco-publico-deste-repositorio
volumes:
- conector-data:/data
labels:
- traefik.enable=true
- traefik.http.routers.opengym-ia.rule=Host(`ia.gym.bruxo.app`)
- traefik.http.routers.opengym-ia.entrypoints=https
- traefik.http.routers.opengym-ia.tls=true
- traefik.http.services.opengym-ia.loadbalancer.server.port=8787
volumes:
conector-data:
```
Se o openGym estiver em outro projeto compose, ligue os dois a uma rede externa em comum e use o
nome do container da API em `OPENGYM_API`. Prefira volume nomeado: o processo roda como `node`
(uid 1000), e um bind mount precisa pertencer a esse uid.
## Desenvolvimento
```sh
npm ci
npm test
```
`npm test` baixa o openGym v1.4.1 com `api/` (`npm ci --omit=optional` dentro dela), sobe
`api/server.js` de verdade na porta 3999 numa pasta temporária, cria o perfil sem navegador (como
os testes do próprio openGym: `db.json` semeado e sessão assinada com o `secret`), gera um código
de pareamento, pareia, chama cada ferramenta pelo cliente MCP do SDK e confere no `GET /api/data`.
`CONECTOR_RAIZ=<pasta>` roda o mesmo teste contra uma cópia montada como a imagem Docker.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues