tasks-mcp-poc
# tasks-mcp-poc
Prueba de concepto de un **gestor de tareas genérico** (al estilo Monday, Jira o Trello) cuya
**única interfaz es MCP**: no hay frontend tradicional, solo modelo de datos persistido en
Postgres, reglas de negocio y un servidor MCP que expone todo como herramientas para ser usadas
por Claude, tanto local (vía stdio) como desplegado en la nube (vía HTTP).
## Dominio
Gestión simple de usuarios y tareas:
- **Usuarios**: `admin` o `member`.
- **Tareas**: título, descripción, estado (`pendiente` | `en_progreso` | `completada`) y usuario asignado.
- El estado persiste en Postgres (Neon) vía Drizzle ORM — ver [Persistencia](#persistencia).
## Reglas de negocio
- Toda operación (salvo `login`) requiere un token de sesión válido obtenido con `login`.
- Las sesiones expiran a la hora de creadas.
- Solo `admin` puede: listar/crear/eliminar usuarios, eliminar tareas, gestionar dependencias.
- `delete_user` no permite eliminar el propio usuario ni uno con tareas asignadas o creadas por
él (hay que reasignarlas o eliminarlas primero); sus sesiones y registros de tiempo sí se
eliminan en cascada.
- Cualquier usuario puede crear tareas (`create_task`): un `admin` puede asignarlas a cualquier
usuario; un `member` solo puede asignarlas a sí mismo o a otro `member` (no a un `admin`).
- Un `member` solo puede ver y actualizar el estado de las tareas que tiene asignadas.
- `admin` puede ver y actualizar cualquier tarea.
- Una tarea puede depender de otras (`add_task_dependency`): mientras alguna dependencia no esté
`completada`, la tarea no puede salir de `pendiente`. No se permiten dependencias circulares.
- Un usuario solo puede tener **un registro de tiempo activo a la vez**: si inicia el timer de
una tarea mientras tiene otra corriendo, la anterior se pausa automáticamente.
- Al pausar (`stop_task_timer`) se puede indicar una razón y, opcionalmente, crear una tarea de
seguimiento asignada a otra persona (ej. "pausa porque espero respuesta de Juan") — esta
creación puntual **no requiere rol admin**, cualquier sesión válida puede reportar un bloqueo.
## Autenticación
Autenticación simple usuario/contraseña:
- Contraseñas guardadas con hash `scrypt` + salt (nunca en texto plano).
- `login(username, password)` devuelve un `token` opaco (UUID) que debe pasarse en el resto
de las herramientas, simulando una sesión.
- `logout(token)` invalida la sesión.
Usuario semilla (solo para esta PoC):
| username | password | rol |
| -------- | ---------- | ----- |
| `admin` | `admin123` | admin |
El resto de los usuarios (`member`) se crean con `create_user` una vez logueado como `admin`.
## Persistencia
- Base de datos Postgres gestionada en [Neon](https://neon.tech), acceso vía `drizzle-orm/neon-http`.
- Schema en `src/db/schema.ts` (tablas `users`, `tasks`, `task_dependencies`,
`task_time_entries`, `sessions`).
- Migraciones con Drizzle Kit:
```bash
npm run db:generate # genera SQL a partir de src/db/schema.ts
npm run db:migrate # aplica migraciones pendientes contra DATABASE_URL
npm run db:seed # crea el usuario admin semilla (idempotente)
npm run db:studio # UI de Drizzle Studio para inspeccionar los datos
```
- `DATABASE_URL` se lee de `.env` en desarrollo (nunca commitear ese archivo) y de un secret
de la plataforma de hosting en producción — ver [Deploy](#deploy).
## Transporte MCP
El servidor de tools (`src/mcp.ts`) es el mismo en ambos casos; solo cambia el transporte:
| Entry point | Transporte | Uso |
| ------------------- | ------------------------------- | -------------------------------------------------------- |
| `src/server.ts` | `StdioServerTransport` | Local, spawneado por Claude Code (`claude mcp add`) |
| `src/httpServer.ts` | `StreamableHTTPServerTransport` | Remoto, servido vía Express en `/mcp` (deploy en Fly.io) |
## Herramientas MCP expuestas
| Herramienta | Rol requerido | Descripción |
| ------------------------ | ------------------------- | --------------------------------------------------------------------------------------------- |
| `login` | público | Autenticarse y obtener token |
| `logout` | sesión válida | Cerrar sesión |
| `whoami` | sesión válida | Ver datos del usuario actual |
| `list_users` | admin | Listar usuarios |
| `create_user` | admin | Crear usuario |
| `delete_user` | admin | Eliminar usuario (no el propio, ni uno con tareas asignadas/creadas) |
| `list_tasks` | sesión válida | Listar tareas (propias o todas si es admin) |
| `create_task` | sesión válida | Crear tarea; member solo puede asignarla a sí mismo u otro member |
| `update_task_status` | dueño de la tarea o admin | Cambiar el estado de una tarea (bloqueado si tiene dependencias sin completar) |
| `start_task_timer` | dueño de la tarea o admin | Iniciar registro de tiempo (pausa automáticamente cualquier otro timer activo del usuario) |
| `stop_task_timer` | dueño de la tarea o admin | Detener/pausar el timer activo; opcionalmente crea una tarea de seguimiento para otra persona |
| `list_task_time_entries` | sesión válida | Listar los registros de tiempo (inicio/fin/motivo) de una tarea |
| `add_task_dependency` | admin | Marcar que una tarea depende de que otra esté completada |
| `remove_task_dependency` | admin | Quitar una dependencia previamente creada |
| `list_task_dependencies` | sesión válida | Listar las tareas que bloquean a una tarea dada |
| `delete_task` | admin | Eliminar una tarea |
## Uso
```bash
npm install
npm run build
```
### Probar con el smoke test incluido
Ejercita todo el flujo (login, permisos denegados, CRUD de tareas) contra el servidor real vía stdio:
```bash
npm run build
npm run smoke
```
### Registrar el servidor en Claude Code
```bash
claude mcp add admin-poc -- node /Users/ckastli/gm2/mcp-poc/dist/server.js
```
Luego, desde Claude, se puede pedir por ejemplo:
> "Inicia sesión como admin (admin/admin123), crea un usuario member nuevo y asígnale una tarea."
Claude usará las herramientas MCP (`login`, `create_user`, `create_task`, etc.) para completar el pedido,
respetando las reglas de negocio y permisos definidos en el servidor.
### Desarrollo
```bash
npm run dev # corre src/server.ts (stdio) directo con tsx, sin compilar
npm run dev:http # corre src/httpServer.ts (Streamable HTTP) en localhost:8080
```
## Deploy
El servidor está desplegado en [Fly.io](https://fly.io) usando el transporte HTTP
(`src/httpServer.ts`), con Docker (`Dockerfile`) y config en `fly.toml`.
```bash
fly deploy -a tasks-mcp-poc
```
### Seguridad: `MCP_API_KEY`
Como el endpoint HTTP queda expuesto públicamente en internet (a diferencia del modo stdio,
que solo corre localmente spawneado por Claude Code), `src/httpServer.ts` exige un bearer
token en todas las requests **antes** de llegar a la lógica de negocio propia del MCP (login,
tokens de sesión, etc. — esto es una capa extra, no un reemplazo):
```
Authorization: Bearer <MCP_API_KEY>
```
Si la variable de entorno `MCP_API_KEY` no está seteada, el middleware no se activa (uso
solo aceptable en desarrollo local). En producción **siempre** debe estar seteada como secret
de la plataforma, nunca en `.env` commiteado ni en el código.
**Generar/rotar el valor:**
```bash
# 1. Generar un valor aleatorio nuevo
API_KEY=$(node -e 'console.log(require("crypto").randomUUID())')
# 2. Cargarlo como secret en Fly (esto reinicia las máquinas automáticamente para aplicarlo)
fly secrets set MCP_API_KEY="$API_KEY" -a tasks-mcp-poc
# 3. Guardar $API_KEY en un gestor de secretos (1Password, Doppler, etc.) —
# Fly no permite volver a leer el valor de un secret ya seteado.
```
Rotarlo periódicamente o ante sospecha de filtración es solo repetir esos tres pasos; no
requiere cambios de código ni un nuevo `fly deploy`.
## Estructura
```
src/
types.ts tipos del dominio (derivados del schema de Drizzle)
crypto-utils.ts hashing de contraseñas
auth.ts login/logout/verificación de sesión y rol
mcp.ts factory createMcpServer(): registra las herramientas
server.ts entry point stdio (uso local con Claude Code)
httpServer.ts entry point Streamable HTTP (uso remoto, Fly.io)
db/
schema.ts tablas de Drizzle (users, tasks, task_dependencies, task_time_entries, sessions)
client.ts cliente Drizzle sobre el driver HTTP de Neon
seed.ts usuario admin semilla (idempotente)
services/
userService.ts reglas de negocio de usuarios
taskService.ts reglas de negocio de tareas (incluye dependencias)
timeTrackingService.ts registro de tiempo trabajado por tarea
drizzle/
*.sql migraciones generadas por drizzle-kit
scripts/
smoke-test.ts cliente MCP que ejercita el flujo completo (vía stdio)
```
## Limitaciones (a propósito, es una PoC)
- Las sesiones (tabla `sessions`) no se purgan automáticamente al expirar, solo al ser usadas.
- Sin rate limiting, recuperación de contraseña, ni auditoría.
- `MCP_API_KEY` es un único secreto compartido (no hay múltiples API keys ni scopes por cliente).
- Las sesiones de transporte MCP viven en memoria del proceso (`src/httpServer.ts`): si la
máquina de Fly se duerme por inactividad o se hace un `fly deploy`, esas sesiones se pierden y
el cliente tiene que reconectar (el token de login, en cambio, persiste en Postgres). Es una
decisión deliberada para mantener las cosas simples en esta PoC — la alternativa (persistir
sesiones MCP en la base) es un cambio real de código, no de configuración.
TDQS
Scored across 16 tools
All tools have clearly distinct purposes. There is no overlap between create/delete/update/list operations for tasks, users, dependencies, and time tracking. Authentication tools are separate.
Most tools follow a consistent verb_noun pattern (e.g., create_task, list_tasks). The authentication tools (login, logout, whoami) break the pattern, but they are standard and easily recognizable.
16 tools is slightly above the typical range but still reasonable for a task management server covering users, tasks, dependencies, time tracking, and authentication. Each tool serves a clear purpose.
The tool set covers core CRUD for users (create, delete, list) and tasks (create, delete, update status, list) but lacks update for users and task details (e.g., edit description, reassign). Also missing a get_task_by_id tool. Time tracking and dependencies are well-covered.