mcp-pedidos
# mcp-pedidos
Servidor MCP para que un cliente consulte sus pedidos desde Claude o ChatGPT:
estado, ítems, total e historial, con **permisos por cliente**. SQLite, cero
dependencias, sin API key.
```text
Cliente: ¿Dónde está mi pedido P-0001?
Agente: P-0001 está enviado desde el 13/11 14:30.
- 4 x Bolsas de harina 000 x 25 kg $98.000,00
- 10 x Levadura fresca x 1 kg $18.900,00
Total: $116.900,00
Historial: pendiente → preparando → enviado
```
---
## El problema
Un negocio chico recibe "¿llegó mi pedido?" por WhatsApp todo el día. La
respuesta ya está en el sistema, pero nadie quiere abrir el sistema. Con este
servidor, el cliente conecta su cuenta a un agente y pregunta en lenguaje
natural; el agente consulta el pedido real, no inventa.
Lo importante no es el CRUD: es el **límite**. Una instancia de cliente no puede
ver pedidos de otro, y el modelo no puede elegir el token: viene en la
configuración del cliente MCP. Si el modelo insiste, el servidor responde "no
existe".
## Qué expone
| Herramienta | Quién | Para qué |
| --- | --- | --- |
| `consultar_pedido` | cliente y admin | Estado, ítems, total e historial de un pedido |
| `listar_pedidos` | cliente y admin | Lista acotada a lo que la sesión puede ver, con filtro por estado |
| `crear_pedido` | cliente | Crea un pedido en estado pendiente |
| `actualizar_estado` | admin | Avanza el pedido respetando la máquina de estados |
| `resumen` | admin | Pedidos por estado y total facturado (sin cancelados) |
## Instalación
Necesitás **Node 22.13+**. No hay dependencias.
```bash
git clone https://github.com/lucabaello1998/mcp-pedidos.git
cd mcp-pedidos
node cli.js semilla
PEDIDOS_TOKEN=cli_laespiga_demo_2026 node cli.js listar
```
### Conectarlo a un cliente MCP
```json
{
"mcpServers": {
"pedidos": {
"command": "node",
"args": ["C:/ruta/a/mcp-pedidos/server.js"],
"env": {
"PEDIDOS_DB": "C:/ruta/a/pedidos.db",
"PEDIDOS_TOKEN": "cli_laespiga_demo_2026"
}
}
}
}
```
Para la instancia del dueño, el mismo servidor con otro token:
```json
"env": {
"PEDIDOS_DB": "C:/ruta/a/pedidos.db",
"PEDIDOS_TOKEN": "<token de admin>",
"PEDIDOS_ADMIN_TOKEN": "<token de admin>"
}
```
Variables:
| Variable | Para qué |
| --- | --- |
| `PEDIDOS_DB` | Ruta de la base SQLite (default `./pedidos.db`) |
| `PEDIDOS_TOKEN` | Token de esta instancia: cliente o admin |
| `PEDIDOS_ADMIN_TOKEN` | Token del dueño, para reconocer la sesión de admin |
## Modelo de datos
```text
clientes (id, nombre, email, token)
pedidos (id, numero, cliente_id, estado, total_centavos, nota, fechas)
items (id, pedido_id, descripcion, cantidad, precio_centavos)
eventos (id, pedido_id, estado, nota, creado_en) -- historial
```
**El dinero va en centavos enteros**, nunca en float: `0.1 + 0.2` no es `0.3`, y
un total no puede depender de eso. La API expone `precio_centavos` y devuelve
`total_centavos`; el `total` en pesos es solo para mostrar.
### Máquina de estados
```text
pendiente → preparando → enviado → entregado
↘ ↘
cancelado cancelado
```
Un pedido entregado no vuelve atrás: si hay que corregir algo, se hace una
devolución, no se reescribe la historia. Las transiciones inválidas devuelven el
motivo y las opciones posibles, no un "error 400".
## Decisiones y por qué
**El token no es un argumento de la herramienta.** Si el token viajara en el
prompt, el modelo podría cambiarlo o el usuario podría pedirle "probá con otro".
Va en la configuración del cliente MCP: una instancia = una identidad. Es la
diferencia entre autenticación y "confiar en el modelo".
**Pedido ajeno = "no existe".** Responder "no autorizado" confirma que el número
existe; con números correlativos eso es un oráculo. Mismo criterio que un login.
**Comparación de tokens en tiempo constante.** No es paranoia de más: es la
misma razón por la que no se compara una contraseña con `===`.
**Máquina de estados explícita.** Un `UPDATE estado = ?` sin validar permite
"entregado → pendiente" y rompe cualquier reporte. Las transiciones están en una
tabla de 5 líneas, legibles y testeables.
**Centavos enteros.** Un pedido con precios en float acumula centavos perdidos;
el bug aparece recién cuando el contador cuadra mal.
**Sin ORM.** Cuatro tablas y cinco consultas: un ORM agregaría una capa para
ahorrar veinte líneas de SQL.
## Límites honestos
- **No hay usuarios ni contraseñas.** El token es la identidad; para producción
haría falta rotación, expiración y un registro de accesos.
- **Un solo negocio por base.** No hay `tenant_id`; para varios negocios se
agrega la columna y se filtra igual que por cliente.
- **Sin pagos ni stock.** El total se calcula, no se cobra ni se descuenta.
- **SQLite.** Bien para una pyme; para varias instancias escribiendo a la vez
hace falta un motor con más concurrencia.
## Tests
```bash
npm test
```
**55 tests** con `node --test`, sin frameworks:
- creación: total en centavos exacto, número correlativo, evento inicial y
rechazo de ítems inválidos (cantidad, precio con decimales, descripción vacía);
- permisos: el cliente ve lo suyo, el ajeno responde "no existe", el admin ve
todo, el cliente no cambia estados ni ve el resumen;
- máquina de estados: recorrido completo, saltos rechazados, estados finales;
- protocolo MCP por stdio: handshake, `tools/list`, `-32601`, `-32700`,
notificaciones sin respuesta y stdout solo con JSON-RPC;
- CLI: semilla idempotente, listar, consultar, resumen y códigos de salida.
## Licencia
MIT. Ver [LICENSE](LICENSE).
TDQS
Scored across 5 tools
Cada herramienta tiene un propósito claramente distinto: consulta individual, listado, creación, cambio de estado y resumen agregado. No hay solapamiento ambiguo entre ellas, incluso consultar_pedido y listar_pedidos se diferencian por alcance y formato de salida.
La mayoría sigue un patrón consistente verbo_sustantivo en español y snake_case: consultar_pedido, listar_pedidos, crear_pedido, actualizar_estado. Solo 'resumen' rompe ligeramente el patrón al ser un sustantivo aislado, pero sigue siendo legible y coherente con el dominio.
Cinco herramientas es un número bien ajustado para un servidor de gestión de pedidos: cubre las operaciones principales sin redundancia. Cada herramienta aporta una capacidad necesaria y diferenciada.
El ciclo de vida del pedido está bien cubierto: creación, consulta, listado, transiciones de estado y resumen del negocio. Faltan operaciones menores como modificar ítems de un pedido existente o una herramienta dedicada de cancelación, aunque la cancelación se resuelve mediante actualizar_estado.