Skip to main content
Glama
README.md
# 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

A4.2/5.0

Scored across 8 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

8 tools is well-scoped for a presentation management service with versioning and deployment. Each tool serves a distinct purpose without redundancy or bloat.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessSyncing