bitacora-mcp
# bitacora-mcp
MCP server en NestJS para **crear, versionar, recuperar y publicar
presentaciones HTML** dirigidas a directivos y PO de Bidcom. Los decks se
persisten versionados y se publican en Google Workspace vía **Google Apps
Script**, con una URL estable, editable y con historial.
## Qué hace
- **18 tools** para crear, editar, versionar, buscar, comparar, validar y
publicar presentaciones HTML (ver tabla completa más abajo).
- **Dos backends de store**, switch via env vars:
- `git` (default): filesystem local, single-user — ideal para desarrollo
- `mongodb`: multi-user, AWS-ready — para deployment centralizado
- **OAuth de Google Workspace** restringido a `bidcom.com.ar` (login real,
no texto libre). Sesiones persistidas en SQLite o MongoDB.
- **Publicación en Apps Script** con idempotencia por commit + access,
reintentos con backoff exponencial, y historial completo de publicaciones.
- **Validación pre-deploy** que unifica chequeos de sandbox + estructura HTML.
## Prerequisitos
- **Node 22** (usa `--env-file` nativo, sin dependencia de `dotenv`)
- **MongoDB 7+** (solo si vas a usar `DECK_STORE=mongodb` o `OAUTH_STORE=mongodb`)
- **Docker** (para levantar MongoDB local fácilmente)
## Setup inicial
```bash
git clone <repo> bitacora-mcp
cd bitacora-mcp
npm install
npm run build
```
## Configuración
Copiá el template de environment y llená los valores:
```bash
cp .env.example .env
```
Editá `.env` con los valores correspondientes (el archivo está en `.gitignore`,
no se commitea). Los campos obligatorios dependen del modo de uso:
### Modo stdio (local, sin login de Google)
Para usar desde Claude Desktop local sin login de Google, no hace falta `.env`
ni credenciales. Las tools corren con `owner` libre (texto):
```bash
npm start # levanta el server por stdio
```
### Modo HTTP (con login de Google Workspace)
Requiere credenciales de OAuth en GCP (ver más abajo). Llená en `.env`:
```env
GOOGLE_WORKSPACE_CLIENT_ID=<client-id>
GOOGLE_WORKSPACE_CLIENT_SECRET=<client-secret>
JWT_SECRET=<openssl rand -hex 32>
```
```bash
npm run start:http # levanta el server por HTTP en http://localhost:3030
```
### Store: git (default) o MongoDB
```env
# Para usar MongoDB (multi-user, AWS-ready):
DECK_STORE=mongodb
OAUTH_STORE=mongodb
MONGODB_URI=mongodb://localhost:27017/bitacora
```
Si no seteás estas vars, el server usa git + SQLite (filesystem local).
## Credenciales de Google (setup una sola vez)
Hay **dos** OAuth clients distintos en el mismo proyecto GCP:
### 1. OAuth client "Desktop app" — para deployar en Apps Script
1. [Google Cloud Console](https://console.cloud.google.com) → proyecto (nuevo o existente)
2. Habilitar **Google Apps Script API** (APIs & Services → Library)
3. Credentials → Create Credentials → OAuth client ID → **Desktop app**
4. Descargar JSON → `~/.bitacora-google/oauth-client.json`
5. Correr el consent flow:
```bash
npm run build
npm run google:authorize
```
Abre el navegador, pedí consent, cachea el refresh token en
`~/.bitacora-google/token.json`. Se refresca solo.
### 2. OAuth client "Web application" — para login de usuarios (modo HTTP)
1. En el mismo proyecto GCP → Credentials → Create Credentials → OAuth client ID
2. Tipo: **Web application** (no Desktop app — ese no tiene redirect URIs editables)
3. **Authorized redirect URIs**: `http://localhost:3030/auth/callback`
4. Anotá Client ID y Client Secret → van en `.env`:
```env
GOOGLE_WORKSPACE_CLIENT_ID=<este>
GOOGLE_WORKSPACE_CLIENT_SECRET=<este>
```
## Levantar MongoDB local (para modo MongoDB)
```bash
docker run -d --name bitacora-mongo -p 27017:27017 mongo:7
```
MongoDB queda en `mongodb://localhost:27017`. Seteá en `.env`:
```env
DECK_STORE=mongodb
OAUTH_STORE=mongodb
MONGODB_URI=mongodb://localhost:27017/bitacora
```
## Pruebas locales
### Smoke test (sin credenciales de Google)
Corre las 18 tools end-to-end contra un cliente de Apps Script mockeado:
```bash
# Modo git (default)
npm run smoke
# Modo MongoDB
DECK_STORE=mongodb MONGODB_URI=mongodb://localhost:27017/bitacora-smoke npm run smoke
```
Los 48 tests cubren: create → update → rollback → deploy → unpublish →
list_deployments, diff, search, archive, validate, fragment update, y cargas
chunked/from-file.
### Probar con MCP Inspector
1. Levantá el server HTTP:
```bash
npm run build
npm run start:http
```
2. En otra terminal, abrí el Inspector:
```bash
npx @modelcontextprotocol/inspector
```
3. En el Inspector: Add Server → Streamable HTTP → URL: `http://localhost:3030/mcp`
4. Al llamar una tool, se abre el navegador para login de Google Workspace.
Logueate con tu cuenta `@bidcom.com.ar`.
5. Las tools van a usar tu email real como `owner` automáticamente.
### Probar con Claude Desktop
1. Configurá `claude_desktop_config.json` (en
`~/Library/Application Support/Claude/` en macOS):
```json
{
"mcpServers": {
"bitacora-remote": {
"url": "http://localhost:3030/mcp"
}
}
}
```
2. Reiniciá Claude Desktop (Cmd+Q y volver a abrir).
3. Pedile a Claude: *"Mostrame las presentaciones que tengo"* — debería
disparar el flujo OAuth la primera vez y listar tus decks.
> **Sesión persistente**: si ya te logueaste desde el Inspector, Claude
> Desktop reusa esa sesión (SQLite/MongoDB la persiste). Para forzar el
> flujo OAuth desde cero, borrá el store de sesiones:
> `rm ~/.bitacora-store/oauth.db` (SQLite) o limpiá las colecciones
> `oauth_*` en MongoDB.
## Conectar a Claude Desktop (modo stdio, sin login)
Para uso local sin login de Google, stdio es más simple:
```json
{
"mcpServers": {
"bitacora": {
"command": "node",
"args": ["/RUTA/ABSOLUTA/bitacora-mcp/dist/main.js"],
"env": {
"DECK_STORE_DIR": "/RUTA/ABSOLUTA/deck-store",
"DECK_STORE": "git"
}
}
}
}
```
O con MongoDB:
```json
{
"mcpServers": {
"bitacora": {
"command": "node",
"args": ["/RUTA/ABSOLUTA/bitacora-mcp/dist/main.js"],
"env": {
"DECK_STORE": "mongodb",
"MONGODB_URI": "mongodb://localhost:27017/bitacora"
}
}
}
}
```
## Tools (18)
### Versionado (13)
| Tool | Qué hace |
|------|----------|
| `presentation_create` | Crea un deck y lo guarda versionado. Devuelve `id` + `version`. Con `partial: true`, reserva el `id` y guarda el HTML recibido como primer chunk sin comitear nada — hay que cerrar con `presentation_append`. |
| `presentation_create_from_file` | Igual que `presentation_create`, pero lee el HTML de un archivo local (`path` absoluto) en vez de tomarlo como argumento. Para decks grandes o con base64 embebido. |
| `presentation_append` | Agrega un chunk de HTML a una carga iniciada con `presentation_create({partial: true})`. `done: true` en el último chunk cierra, valida y comitea el deck completo. |
| `presentation_update` | Nueva versión con HTML y/o metadata nuevos. Con `fragment: true`, inyecta el HTML antes de `</body>` sin reemplazar el documento completo. |
| `presentation_update_from_file` | Igual que `presentation_update`, pero lee el HTML nuevo de un archivo local. |
| `presentation_get` | HTML + metadata en HEAD o en un `version` histórico. La descripción le pide al asistente mostrar el `html` como Artifact. |
| `presentation_list` | Lista los decks, filtrable por `owner`. Excluye archivados por defecto (`includeArchived: true` para verlos). |
| `presentation_list_versions` | Historial de commits/versiones de un deck. |
| `presentation_rollback` | Vuelve a un `version` anterior creando una versión nueva (no destructivo). |
| `presentation_search` | Búsqueda full-text sobre título, tags y contenido HTML. Devuelve matches con snippet y `matchedIn`. |
| `presentation_diff` | Dif textual + visual HTML side-by-side entre dos versiones. Resuelve "¿qué cambió entre la versión que aprobó el director y la actual?". |
| `presentation_archive` | Soft-delete no destructivo: marca el deck como archivado, lo saca de `list`/`search`. |
| `presentation_unarchive` | Restaura un deck archivado. |
| `presentation_validate` | Reporte estructurado pre-deploy: DOCTYPE, mixed content, `<base target>`, charset, viewport, scripts inline, event handlers, múltiples `<body>`. Devuelve `canDeploy`. |
### Publicación (5)
| Tool | Qué hace |
|------|----------|
| `presentation_deploy` | Publica un `version` (default HEAD) como web app de Apps Script. `access` opcional controla quién puede verla (`MYSELF`/`DOMAIN`/`ANYONE`/`ANYONE_ANONYMOUS`, default `DOMAIN`). Idempotente por commit + access. |
| `presentation_get_deployment` | Devuelve el estado de publicación actual (commit, scriptId, deploymentId, url). |
| `presentation_list_deployments` | Historial completo de publicaciones por deck (incluye despublicadas), para auditoría. |
| `presentation_unpublish` | Despublica: borra el deployment de Apps Script (la URL deja de servir), marca el registro con `unpublishedAt`. No destructivo. |
| `presentation_get_deployment` | Estado de publicación actual de un deck. |
## Decks grandes
Dos problemas distintos, dos soluciones distintas:
**1. El HTML ya existe como archivo en disco** → `create_from_file` /
`update_from_file`. El server lo lee directo del filesystem, byte a byte. El
modelo nunca reproduce el contenido, así que no importa cuán grande sea ni
si tiene base64 embebido.
**2. El HTML lo está generando el modelo** y no entra en una sola tool call
→ `create({partial: true})` + `append` por chunks. Recién en `done: true` se
normaliza y comitea, igual que un `create` de una sola llamada.
## Arquitectura
```
src/
core/ # DeckService (normalize/escHtml), DeckValidateService
store/ # DeckStore interface + GitSpecStore | MongoDeckStore
auth/ # WorkspaceDomainGuard, SqliteOAuthStore, MongoOAuthStore
presentations/ # PresentationsService + Controller (13 tools), PendingUpload
deployer/ # AppsScriptClient (real/mock), SandboxTransform, DeployerService (5 tools)
shared-tools.module.ts # controllers + providers, importado por stdio y HTTP
app.module.ts # bootstrap stdio (sin auth)
http-app.module.ts # bootstrap HTTP (con OAuth de Workspace)
main.ts / main-http.ts # entry points
```
**Dos modos de store, dos modos de auth:**
| Componente | Default (local) | MongoDB (multi-user/AWS) |
|------------|----------------|--------------------------|
| Decks | `GitSpecStore` (filesystem) | `MongoDeckStore` (colección `decks`) |
| OAuth sessions | `SqliteOAuthStore` (archivo) | `MongoOAuthStore` (colecciones `oauth_*`) |
Switch via `DECK_STORE` y `OAUTH_STORE` env vars.
## Notas de stack
- `@rekog/mcp-nest` **v2** — API `McpStrategy` + `@McpController`
- `@rekog/mcp-nest-auth` **v2** — servidor de autorización OAuth 2.1/MCP
embebido (`McpAuthModule` + `GoogleOAuthProvider`)
- Node 22 con `--env-file=.env` nativo (sin `dotenv`)
- `mongodb` driver oficial (sin Mongoose ni ODM)
- `simple-git` para el store git, `better-sqlite3` para el store de sesiones
- `zod` v4 para schemas de las tools
- TypeScript 7.x, `module`/`moduleResolution`: `nodenext`TDQS
Scored across 8 tools
Each tool targets a distinct resource and action: create, update, get, list_versions, list, rollback, deploy, and get_deployment. The boundaries are clear, e.g., presentation_get for content vs presentation_get_deployment for deployment status.
All tools follow the presentation_verb pattern consistently, using straightforward verbs like create, update, get, list, rollback, deploy. Even sub-actions like list_versions and get_deployment maintain the pattern.
8 tools is well-scoped for a presentation management service with versioning and deployment. Each tool serves a distinct purpose without redundancy or bloat.
The lifecycle is well covered with create, read, update, versioning, rollback, and deployment. The only notable gap is the absence of a delete/purge operation for presentations, which agents might need for full lifecycle management.