Luca MCP
README.md
> **Copia personal.** Este repositorio es una copia de
> [JosephRobles23/luca](https://github.com/JosephRobles23/luca) ([lucaa.lat](https://lucaa.lat)),
> creado por Joseph Robles y distribuido bajo licencia MIT.
> El historial de commits y el archivo [`LICENSE`](LICENSE) originales se conservan sin cambios.
> Todo el crédito del trabajo original es de su autor.
<p align="center">
<img src="apps/web/public/Portada-luca.png" alt="Luca — automatización de finanzas personales" width="720">
</p>
<h1 align="center">Luca</h1>
<p align="center">
Tus gastos de BCP y Yape, ordenados en <b>tu</b> Google Sheet. Gratis, sin base de datos y sin configurar nubes.
</p>
<p align="center">
<a href="https://lucaa.lat">lucaa.lat</a> ·
<a href="#cómo-funciona">Cómo funciona</a> ·
<a href="#arquitectura">Arquitectura</a> ·
<a href="#desarrollo">Desarrollo</a> ·
<a href="CONTRIBUTING.md">Contribuir</a> ·
<a href="SECURITY.md">Seguridad</a>
</p>
<p align="center">
<a href="LICENSE"><img alt="Licencia MIT" src="https://img.shields.io/badge/licencia-MIT-d9623b"></a>
<img alt="Node ≥ 20" src="https://img.shields.io/badge/node-%E2%89%A520-1d1a17">
<img alt="Apps Script" src="https://img.shields.io/badge/Google-Apps%20Script-4285F4">
<img alt="Next.js 16" src="https://img.shields.io/badge/Next.js-16-000000">
<img alt="Cloudflare Workers" src="https://img.shields.io/badge/Cloudflare-Workers-F38020">
<img alt="MCP" src="https://img.shields.io/badge/MCP-Claude%20%7C%20ChatGPT-7a7268">
</p>
---
Luca lee los correos de notificación de **BCP** y **Yape** que ya llegan a tu Gmail, los convierte en movimientos
normalizados y categorizados dentro de una **Google Sheet que es tuya**, y te los muestra en un dashboard web.
Opcionalmente captura los yapeos recibidos desde las notificaciones del **iPhone** y te deja consultar o registrar
gastos desde **Claude o ChatGPT** mediante un servidor **MCP**.
> **Principio rector: los datos viven y se procesan en el Google del usuario.** Luca no guarda transacciones,
> tokens de Google ni claves de API en su infraestructura. La web no tiene base de datos.
## Demo en video
<table>
<tr>
<td width="50%" align="center">
<a href="https://pub-9c0cbd6f24354fc589d1b895be70355d.r2.dev/luca-video/luca-demo-narrado.mp4"><img src="docs/img/video-demo.jpg" alt="Demo narrado de Luca (3:52)"></a>
</td>
<td width="50%" align="center">
<a href="https://pub-9c0cbd6f24354fc589d1b895be70355d.r2.dev/luca-video/luca-pitch-mujer.mp4"><img src="docs/img/video-pitch.jpg" alt="Pitch de Luca (2:22)"></a>
</td>
</tr>
<tr>
<td valign="top">
<b><a href="https://pub-9c0cbd6f24354fc589d1b895be70355d.r2.dev/luca-video/luca-demo-narrado.mp4">Demo narrado · 3:52</a></b><br>
El producto real de punta a punta: copiar la plantilla, el panel en lucaa.lat, el dashboard dentro de la Sheet,
las conexiones y el conector MCP preguntándole a ChatGPT por tus gastos.
</td>
<td valign="top">
<b><a href="https://pub-9c0cbd6f24354fc589d1b895be70355d.r2.dev/luca-video/luca-pitch-mujer.mp4">Pitch · 2:22</a></b><br>
La idea en dos minutos: tu Sheet es la base de datos, tu Google es el backend y la web solo renderiza.
Animación generada por código, cuadro a cuadro.
</td>
</tr>
</table>
## Tabla de contenidos
- [Demo en video](#demo-en-video)
- [Características](#características)
- [Cómo funciona](#cómo-funciona)
- [Arquitectura](#arquitectura)
- [Privacidad y modelo de seguridad](#privacidad-y-modelo-de-seguridad)
- [Empezar como usuario](#empezar-como-usuario)
- [Desarrollo](#desarrollo)
- [Estructura del repositorio](#estructura-del-repositorio)
- [Releases](#releases)
- [Documentación](#documentación)
- [Contribuir](#contribuir)
- [Licencia](#licencia)
## Características
- **Ingesta automática desde Gmail**: parsers deterministas para 9 tipos de correo de BCP y Yape (consumos con
tarjeta, transferencias, pagos con QR, servicios, recargas, yapeos…), con escaneo cada 15 minutos e
importación histórica reanudable.
- **Categorización en cascada**: elección del usuario → reglas → caché de comercios aprendida → LLM opcional.
Al LLM solo le llega `comercio + monto` ([ADR-004](docs/architecture/adr-004-categorizacion-y-llm.md)).
- **Modelo de movimientos** con tipos (`expense`, `income`, `transfer_in`, `internal_transfer`…), P2P y
multi-moneda PEN/USD ([ADR-005](docs/architecture/adr-005-modelo-de-movimientos.md)).
- **Web en [lucaa.lat](https://lucaa.lat)**: resumen del mes, categorías, tendencias de 6 meses, comercios
principales, recategorización, alta manual y gestión de conexiones. Lee y escribe la Sheet **desde el navegador**.
- **Dentro de Google Sheets**: menú Luca, panel lateral, guía paso a paso y Dashboard con gráficos SVG.
- **iPhone (iOS 27)**: un atajo envía los yapeos recibidos **directo** a tu Apps Script, sin pasar por Luca
([ADR-003](docs/architecture/adr-003-canal-iphone-directo.md)).
- **Conector MCP** para Claude y ChatGPT: `get_summary`, `category_breakdown`, `top_merchants`,
`list_transactions`, `budget_status` y `add_expense` ([services/luca-mcp](services/luca-mcp/README.md)).
- **Extractor LLM opt-in** para correos no reconocidos, que recibe el texto enmascarado
([ADR-008](docs/architecture/adr-008-extractor-llm-opt-in.md)).
## Cómo funciona
Cada usuario tiene **su propia copia** de una Sheet plantilla. Esa copia lleva un script ligado (el *stub*) que
delega toda la lógica en una librería compartida de Apps Script (**LucaLib**). El script corre **con la identidad
del usuario** y es el único componente que toca Gmail y los montos.
```mermaid
flowchart LR
subgraph Fuentes["Fuentes"]
G["Correos BCP / Yape<br/>(Gmail del usuario)"]
I["Notificación Yape<br/>(Atajo iOS)"]
W["Alta manual<br/>(web)"]
A["add_expense<br/>(Claude / ChatGPT)"]
end
subgraph GAS["Apps Script del usuario (stub + LucaLib)"]
P["Parsers deterministas<br/>correo · push"]
D["Deduplicación<br/>id + monto/minuto"]
C["Categorización<br/>usuario → reglas → caché → LLM"]
end
S[("Google Sheet del usuario<br/>Movimientos · Categorías · Comercios · Ajustes")]
G -->|trigger cada 15 min| P
I -->|POST /exec?events=1| P
A -->|Worker → POST /exec?mcp=1| D
P --> D --> C --> S
W -->|Sheets API desde el navegador| S
```
## Arquitectura
### Vista de componentes
```mermaid
flowchart TB
U(["Usuario"])
subgraph Google["Google del usuario"]
direction TB
SH[("Sheet 'Luca'<br/>copia de la plantilla")]
STUB["Stub ligado a la Sheet<br/>menú · triggers · doGet/doPost"]
LIB["LucaLib<br/>librería Apps Script compartida"]
GM["Gmail<br/>(gmail.readonly)"]
PROPS["UserProperties<br/>secretos del usuario"]
STUB --> LIB
LIB --> SH
LIB --> GM
LIB --> PROPS
end
subgraph Luca["Infraestructura de Luca (sin datos de usuario)"]
direction TB
WEB["Web Next.js<br/>lucaa.lat · Vercel<br/>Auth.js + Google"]
MCP["Worker luca-mcp<br/>mcp.lucaa.lat · Cloudflare<br/>OAuth 2.1 · D1 · KV"]
end
IA(["Claude / ChatGPT"])
IPH(["iPhone<br/>Atajos iOS 27"])
LLM(["Proveedor LLM<br/>(opcional, key del usuario)"])
U -->|navegador| WEB
WEB -->|"Sheets / Drive API<br/>token drive.file en el navegador"| SH
WEB -.->|GET /meta| MCP
IA -->|"MCP (Streamable HTTP)"| MCP
MCP -->|"POST /exec?mcp=1<br/>secreto por tenant"| STUB
IPH -->|"POST /exec?events=1<br/>token por dispositivo"| STUB
LIB -.->|"comercio + monto"| LLM
```
| Componente | Tecnología | Qué guarda |
|---|---|---|
| **LucaLib** (`gas/shared`) | Google Apps Script | Nada propio: escribe en la Sheet y en `UserProperties` del usuario |
| **Stub** (`gas/stub`) | Apps Script ligado a la plantilla | Triggers y `doGet`/`doPost` del proyecto contenedor |
| **Web** (`apps/web`) | Next.js 16, React 19, Auth.js v5, Tailwind 4 | Nada en servidor: tokens de Google solo en la cookie JWT cifrada |
| **Worker** (`services/luca-mcp`) | Cloudflare Workers, `workers-oauth-provider`, `agents`, D1, KV | `tenantId → {execUrl, secreto}` y códigos de pairing con TTL; estado OAuth |
Las decisiones de arquitectura están en [`docs/architecture/`](docs/architecture/) (ADR-001…008).
### Onboarding en tres pasos
```mermaid
sequenceDiagram
autonumber
actor U as Usuario
participant W as Web (lucaa.lat)
participant D as Google Drive
participant S as Sheet + Apps Script
U->>W: Entra con Google
W->>D: Picker sobre la plantilla y files.copy (drive.file)
D-->>W: Copia con appProperties {luca: ledger}
W-->>U: "Abre tu Sheet y autoriza"
U->>S: Menú Luca → Autorizar
S->>S: Consentimiento, trigger cada 15 min,<br/>importación del último mes
U->>W: Vuelve a la web
W->>D: files.list (localiza su Sheet)
W->>S: Lee Movimientos y Ajustes (Sheets API)
W-->>U: Dashboard
opt Paso 3 · conexiones
U->>S: Publica el Web App y conecta iPhone o IA
end
```
### Conector MCP: pairing y llamada a una tool
El Worker es un **relevo sin estado de datos**: traduce MCP a una llamada al Web App del usuario y no ve ni guarda
movimientos ([ADR-001](docs/architecture/adr-001-acceso-mcp-a-datos.md)).
```mermaid
sequenceDiagram
autonumber
actor U as Usuario
participant S as Apps Script (/exec)
participant M as Worker luca-mcp
participant C as Claude / ChatGPT
U->>S: Sidebar → "Conectar IA"
S->>M: POST /enroll {webAppUrl, secret}
M->>S: Challenge HMAC contra /exec
S-->>M: Firma válida
M-->>S: Código de pairing (8 caracteres, un solo uso, 10 min)
U->>C: Añade el conector mcp.lucaa.lat/mcp
C->>M: OAuth 2.1 (DCR + PKCE) → /authorize
U->>M: Pega el código
M-->>C: Access token (props.tenantId, TTL 24 h)
C->>M: tools/call get_summary {month}
M->>S: POST /exec?mcp=1 {op, secret, args}
S-->>M: {ok: true, ...} calculado sobre la Sheet
M-->>C: Resultado de la tool
```
## Privacidad y modelo de seguridad
- **Sin base de datos de usuarios.** El vínculo usuario ↔ Sheet lo resuelve Drive (`appProperties`); la web solo
refresca tokens que viven en la cookie cifrada del usuario ([ADR-006](docs/architecture/adr-006-web-y-onboarding.md)).
- **Permisos mínimos.** La web usa `drive.file` (solo los archivos que Luca crea o que el usuario elige). El script
del usuario usa `gmail.readonly` para leer notificaciones bancarias.
- **Secretos por usuario** en `PropertiesService.getUserProperties()` de su propio script: API key del LLM, token
del iPhone y secreto del conector MCP. Nunca en las Script Properties de la librería.
- **El Worker no ve movimientos.** Guarda solo `tenantId → {execUrl, secreto}` y códigos de pairing que expiran.
- **LLM opcional y acotado.** Solo `comercio + monto`; el extractor de correos desconocidos es opt-in y enmascara el
texto antes de enviarlo.
- **Sin analítica ni rastreadores.**
¿Encontraste una vulnerabilidad? Repórtala en privado según [SECURITY.md](SECURITY.md).
## Empezar como usuario
1. Entra en **[lucaa.lat](https://lucaa.lat)** con tu cuenta de Google y pulsa **Crear mi Sheet**. Se copia la
plantilla a tu Drive.
2. Abre la Sheet → menú **Luca → Autorizar** y acepta los permisos. Luca importa el último mes y queda escaneando
cada 15 minutos.
3. Vuelve a la web para ver tu dashboard. Opcional: **Activar conexiones** para el iPhone
([guía](docs/guides/guia-atajos-ios27-yape.md)) y para tu IA (código de pairing → conector MCP).
No necesitas cuenta de GCP ni configurar ninguna nube.
## Desarrollo
### Requisitos
- Node.js ≥ 20 y npm.
- Para publicar LucaLib o el stub: [`clasp`](https://github.com/google/clasp) autenticado.
- Para el Worker: [`wrangler`](https://developers.cloudflare.com/workers/wrangler/) (se instala con el paquete).
- Para la web en local: un cliente OAuth de Google y `apps/web/.env.local` (copia de
[`apps/web/.env.example`](apps/web/.env.example)). Guía: [`docs/guides/guia-dns-vercel-oauth.md`](docs/guides/guia-dns-vercel-oauth.md).
### Puesta en marcha
```bash
git clone https://github.com/JosephRobles23/luca.git
cd luca
npm install --prefix apps/web
npm install --prefix services/luca-mcp
npm test # tests Node: Apps Script (harness vm), lógica web y Worker
npm run web:dev # web en http://localhost:3000 (necesita apps/web/.env.local)
npm run dev:mock --prefix apps/web # web con datos simulados, sin Google (LUCA_MOCK=1)
```
`npm test` no toca Google: las pruebas de Apps Script corren en un sandbox `vm` de Node con mocks de los servicios
(`tests/gas-harness.mjs`) y fixtures de correo sintéticos y anonimizados.
### Comandos
| Comando | Qué hace |
|---|---|
| `npm test` | Todos los tests unitarios (Apps Script, web y Worker) |
| `npm run e2e --prefix apps/web` | E2E con Playwright sobre la web en modo mock |
| `npm run mcp:typecheck` | `tsc` del Worker |
| `npm run web:build` | Build de producción de la web |
| `npm run release:check` | Verifica que la versión de LucaLib coincide en los 4 sitios |
| `npm run lib:push` / `lib:version` | `clasp push` y nueva versión de LucaLib |
| `npm run stub:push` | `clasp push` del stub a la plantilla |
| `npm run mcp:deploy` | Despliega el Worker |
### Convenciones
- **Apps Script** (`gas/`): sin `import/export`; funciones privadas con sufijo `_`; toda llamada de la UI pasa por
`lucaRun` → `dispatch` (lista blanca). Un archivo nuevo en `gas/shared` se añade a `RUNTIME_FILES` en
`tests/gas-harness.mjs`.
- **Web**: lógica pura en `apps/web/src/lib/*.ts` con tests `.test.mjs`; los componentes solo presentan.
- **Parsers deterministas primero**; el LLM es siempre opcional.
- **Fixtures** sintéticos y anonimizados en `tests/fixtures`. Los correos reales nunca se versionan.
El detalle está en [CLAUDE.md](CLAUDE.md) (guía para agentes y colaboradores) y el vocabulario en [CONTEXT.md](CONTEXT.md).
## Estructura del repositorio
```text
gas/shared/ LucaLib: parsers, ledger, escaneo Gmail, categorización, LLM opcional, MCP, UI de la Sheet
gas/stub/ script ligado a la plantilla (bootloader delgado: menú, triggers, doGet/doPost)
apps/web/ web Next.js (lucaa.lat): login, onboarding, dashboard, conexiones; sin base de datos
services/luca-mcp/ Worker de Cloudflare (mcp.lucaa.lat): OAuth 2.1, pairing y tools MCP
scripts/ release-check, eml-check, build-previews
tests/ harness vm con mocks de Apps Script y fixtures sintéticos
spikes/ experimentos de validación (S1–S7) y sus resultados
docs/ arquitectura y ADRs, guías, research, discovery, estado del desarrollo
```
## Releases
Una release publica una versión nueva de LucaLib y repite su número en cuatro sitios (`LUCA_VERSION`, el manifiesto
del stub, `wrangler.toml` y `.env.example`). El procedimiento completo está en
[`.claude/skills/deploy-luca/SKILL.md`](.claude/skills/deploy-luca/SKILL.md).
Las copias existentes de los usuarios **no se actualizan solas**: la web y el panel de la Sheet avisan cuando hay
una versión nueva y muestran cómo subirla ([ADR-006 §5](docs/architecture/adr-006-web-y-onboarding.md)).
## Documentación
| Tema | Dónde |
|---|---|
| Arquitectura base, ADRs y plan | [`docs/architecture/`](docs/architecture/) |
| Estado del desarrollo | [`docs/dev/status.md`](docs/dev/status.md) |
| Formatos reales de correos BCP/Yape | [`docs/discovery/formatos-correos-bcp-yape.md`](docs/discovery/formatos-correos-bcp-yape.md) |
| DNS, Vercel y clientes OAuth | [`docs/guides/guia-dns-vercel-oauth.md`](docs/guides/guia-dns-vercel-oauth.md) |
| Consola de GCP (operador) | [`docs/guides/gcp-consola-oauth-luca.md`](docs/guides/gcp-consola-oauth-luca.md) |
| Captura de yapeos en iPhone | [`docs/guides/guia-atajos-ios27-yape.md`](docs/guides/guia-atajos-ios27-yape.md) |
| Servidor MCP | [`services/luca-mcp/README.md`](services/luca-mcp/README.md) |
| Sistema de diseño | [`DESIGN.md`](DESIGN.md) |
| Investigaciones con fuentes | [`docs/research/`](docs/research/) |
## Contribuir
Las contribuciones son bienvenidas. Lee [CONTRIBUTING.md](CONTRIBUTING.md) antes de abrir un PR. Las reglas que no se
negocian: los datos se quedan en el Google del usuario, el usuario final no configura ninguna nube y los secretos
nunca entran al repositorio.
## Licencia
[MIT](LICENSE) © 2026 Joseph Robles.
Luca no está afiliado a BCP, Yape, Google, Anthropic, OpenAI ni Cloudflare. Las marcas pertenecen a sus dueños.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues