Para público técnico
Cómo funciona el servicio
MathTrail se sitúa entre el chat que una familia ya usa y su Google Drive. El modelo del chat escribe cada problema, y el servicio decide si llega hasta el niño o la niña. Es un solo programa: no llama a ningún modelo propio y no guarda nada entre peticiones.
- Un binario de Go en Cloud Run
- Sin estado
- Sin base de datos
- Sin llamadas propias a LLM
- MCP 2026-07-28 · MCP Apps
Cliente de chat1 · cliente
- Claude
- ChatGPT
- otros hosts de MCP Apps
El modelo del chat escribe el problema, y la tarjeta se lo muestra al niño o la niña.
MCP · llamadas a herramientas por HTTPS
MathTrail en Google Cloud2 · ejecución
Cloud Run · un binario de Go · sin estado
El núcleo: cálculo puro
- La regla
- Puntuaciones
- Comprobaciones
- Solucionador
- Contenido
A su alrededor: OAuth, el servidor MCP, la tarjeta, el sellado, los límites.
Drive API v3 · con el propio token de la familia
Google Drive3 · almacenamiento
MathTrail/mathtrail-profile.json
Un archivo por cada niño o niña, en el propio Drive de la familia: lo único que se guarda a largo plazo.
01
El mapa
Quién habla con quién. Ni el niño o la niña ni la persona adulta llegan directamente a MathTrail: hablan con el host del chat. Todo lo que hay en el bloque central es parte de un mismo proceso, no un servicio aparte. Elija cualquier parte para ver qué hace y con qué se conecta.
02
Una clase, llamada a llamada
El modelo y la tarjeta llaman a las mismas herramientas y se apoyan en una única fuente de verdad: el archivo del perfil en el Drive de la familia. Para que la clase siga, no necesitan decirse nada.
Problema
- Persona adulta
- Tarjeta
- Modelo del host
- MathTrail
- Drive
1Persona adulta → Modelo del host: «un problema nuevo, por favor»
2Modelo del host → MathTrail: next_task
El host dibuja la tarjeta en cuanto empieza la llamada: «Preparando el siguiente problema…»
límites · la regla elige el tema, el nivel y la dificultad
3MathTrail → Drive: escribir la petición abierta
4MathTrail → Modelo del host: la petición está abierta: obtén el paquete
5Modelo del host → MathTrail: get_package
6MathTrail → Modelo del host: encargo · 3 problemas de referencia · trampas · plantillas · ejemplos de dibujos
El modelo escribe el problema, un solucionador en Starlark y una autocomprobación
7Modelo del host → MathTrail: submit_task: hasta 3 intentos
comprobaciones · el solucionador debe encontrar exactamente una opción correcta
8MathTrail → Drive: el problema pasa a ser el actual, con su respuesta sellada
9Tarjeta → MathTrail: read_task: la tarjeta pregunta cada 4 s
10MathTrail → Tarjeta: texto · dibujo · cinco opciones · pista, sin la respuesta
Cada llamada lee además el perfil desde Drive; esas lecturas se omiten. La tarjeta que espera nunca ve el paquete: sus problemas de referencia llevan sus respuestas. Mientras el niño o la niña resuelve, el modelo escribe por adelantado el siguiente problema con prepare_task, y cuando se pide otro, next_task lo pone enseguida en una tarjeta nueva, sin esta espera.
Respuesta
- Persona adulta
- Niño o niña
- Tarjeta
- Modelo del host
- MathTrail
- Drive
1Niño o niña → Tarjeta: pulsa C
2Tarjeta → MathTrail: submit_answer: el chat no gasta ningún turno
solo la primera respuesta · ahora se abre el sello · θ y δ solo se mueven según si es correcta
3MathTrail → Drive: escribir las puntuaciones y el historial
4MathTrail → Tarjeta: correcta o no · la trampa detrás de C · el dibujo de la solución · los pasos · la puntuación antes → después
La tarjeta se convierte en el repaso de la respuesta, en el lugar del problema
5Tarjeta → Modelo del host: ui/update-model-context: una línea, que se lee con el siguiente mensaje de la persona adulta
6Persona adulta → Modelo del host: «¿por qué hay un poste más?»
7Modelo del host → Persona adulta: responde sobre este mismo problema, completando la tarjeta
La pulsación registra la respuesta antes de que se explique nada, y la tarjeta se convierte enseguida en el repaso de la respuesta. El chat no gasta ningún turno y el modelo no dibuja nada. El modelo lee la línea de la tarjeta con el siguiente mensaje de la persona adulta y responde a una pregunta sobre el problema sin volver a contar lo que muestra la tarjeta. Si la línea que el host envía al modelo se pierde, el modelo se pone al día en su siguiente llamada.
Acceso
- Navegador de la familia
- Host
- MathTrail
- Drive
1Host → MathTrail: POST /mcp, sin token
2MathTrail → Host: 401 · dónde están los metadatos
3Host → Navegador de la familia: abre /oauth/authorize · PKCE S256
4Navegador de la familia → MathTrail: authorize
Una pantalla de consentimiento propia: qué cliente, y adónde devolverá a la persona adulta
5Navegador de la familia → Google: inicio de sesión con Google · solo drive.file
6Google → Navegador de la familia: de vuelta, con el código de Google
7Navegador de la familia → MathTrail: /oauth/callback
8MathTrail → Google: intercambiar el código
9MathTrail → Drive: /about: ¿de quién es este Drive?
id de usuario = HMAC de la cuenta de Drive · los tokens de Google, sellados en nuestro código
10MathTrail → Navegador de la familia: 302 · code = mt1.c.…
11Navegador de la familia → Host: devuelve el código
12Host → MathTrail: /oauth/token · código + verificador
13MathTrail → Host: acceso 15 min · actualización 30 días
La persona adulta inicia sesión una sola vez. El servicio no guarda nada de este intercambio: la petición viaja sellada en el parámetro state, y los tokens de Google, en un código sellado.
El problema actual como máquina de estados
Cada regla de «solo una vez» es un campo del perfil, que se comprueba dentro de la misma lectura, cálculo y escritura. Ninguna otra cosa recuerda nada.
nonenext_taskencargo listorequestedsubmit_task ✓sellado, entregadoissuedsubmit_answerregistrada una vezanswered
requested ↺rechazado, quedan intentos: no más de tres por peticiónrequested → nonetres rechazos, o 15 minutos desde que se abrió la petición; no se gastan problemas del día, y tres rechazos cuentan como una petición fallida del díaissued → requested«Otro problema» antes de una respuesta: se registra como en blanco y la puntuación no cambiaanswered ↺cualquier respuesta posterior —desde una segunda pestaña o desde el modelo— recibe el primer resultado y no escribe nadaanswered → requestedel siguiente problema, en una tarjeta nueva; el ya respondido deja de ser el actualanswered → issuedel siguiente problema ya estaba escrito: next_task lo pone enseguida en una tarjeta nueva, saltándose requestedissued → noneel sello ya no se puede abrir porque la clave ha rotado dos veces; no se registra nada
03
Dónde vive el estado
Todo lo que un servidor con estado guardaría en una base de datos lo guarda otro: en un token sellado que conserva el chat, en una cookie del navegador de la familia o en el propio Drive de la familia. Reinicie el servicio o arranque una segunda copia: no se pierde nada.
El propio MathTrail
Nada
Ni cuentas, ni sesiones, ni registros de clientes, ni tokens de Google. Cada petición lleva todo lo que necesita.
El almacén de tokens del chat
Tokens sellados, opacos para el host
- Token de acceso
- 15 min
- Token de actualización
- 30 días, ≤ 90
Dentro van sellados los tokens de Google, un id de usuario opaco para los límites y el país desde el que la persona adulta inició sesión.
El navegador de la familia
Dos cookies, sin sesión
- Clientes aprobados
- 180 días
- Protección del inicio de sesión (CSRF)
- 10 min
Perderlas cuesta una pantalla de consentimiento más, o volver a iniciar sesión.
El Drive de la familia
Lo único que se guarda a largo plazo
mathtrail-profile.json- hasta que se elimine
El perfil, las puntuaciones y el problema actual, con su respuesta sellada. La persona adulta puede abrir el archivo, y Drive guarda su historial.
Secret Manager
Se leen una vez, al arrancar una instancia
- Claves de selladotras una rotación, la clave anterior se sigue aceptando
- 90 días
- Secreto de cliente de Google
- fijado
La memoria de la instancia
Solo cachés: se pierden al reiniciar, y nada se rompe
- Contadores de ritmo de peticiones
- minutos
- Dónde está el archivo del perfil
- 10 min
- Documentos de los clientes
- de 5 min a 24 h
Lo que la v1 deja fuera a propósito
- Una base de datos
- Colas
- Un almacén de sesiones
- Una caché compartida
- Un banco de problemas ya escritos
- Un cliente de API de LLM
- REST y OpenAPI
04
El archivo del perfil
Un niño o niña, un archivo JSON: no una caché de alguna verdad guardada en un servidor, sino la verdad misma. Cada bloque tiene un tope que se respeta en cada escritura, así que el archivo nunca crece sin límite: uno normal no pasa de la meta, y uno cerca de sus topes la supera. Los problemas pasados se guardan como huellas, no como texto.
Tamaño del archivo
≈ 61 KB / ≈ 79 KB
| Bloque | Qué contiene | Quién lo escribe | Tamaño | |
|---|---|---|---|---|
| Huellas | Hasta 200 esbozos de los problemas entregados, para comprobar los casi duplicados; sin texto, sin tema, sin fecha | next_tasksubmit_task | 27 KB / 29 KB | |
| Historial de puntuaciones | Dónde estaban θ y el δ del tema antes de cada respuesta reciente, y θ y cada tema al comienzo de cada uno de los últimos 7 días con respuestas | submit_answer | 14 KB / 15 KB | |
| Puntuaciones y temas | θ y el punto de partida del niño o la niña; para cada tema: δ, las respuestas, las trampas en las que cayó, el dominio y su nivel | next_tasksubmit_tasksubmit_answer | 8 KB / 14 KB | |
| Respuestas recientes | Las últimas 20 respuestas y problemas en blanco, y nunca menos de las 5 respuestas que lee la regla | next_tasksubmit_answer | 4 KB / 5 KB | |
| Problema actual, sellado | La respuesta, la trampa detrás de cada opción incorrecta, la solución y el solucionador, en una sola cadena opaca | next_tasksubmit_tasksubmit_answer | 4 KB / 8 KB | |
| Problema actual, abierto | Lo que muestra la tarjeta: el texto, el dibujo, cinco opciones, la pista y, después de responder, la propia respuesta | next_tasksubmit_tasksubmit_answer | 2 KB / 4 KB | |
| Petición abierta | Mientras se escribe el problema: su id, el encargo, los intentos usados y quién eligió el tema | next_taskprepare_tasksubmit_task | 1,5 KB / 2 KB | |
| Servicio y niño o niña | La versión del esquema, un UUID aleatorio, la revisión; el seudónimo, el grado, los intereses, las anotaciones; los contadores del día | save_profileedit_profilecada escritura | 0,6 KB / 1,5 KB |
Un problema escrito por adelantado está junto al actual y pesa más o menos lo mismo. El archivo no contiene los textos de los problemas pasados, ni la respuesta en claro hasta que el niño o la niña haya respondido, ni nada que nombre a la persona adulta, ni una etapa: las etapas se calculan cuando se muestran. Drive no tiene escritura condicional, así que el archivo se vuelve a leer antes de guardar y, si ha cambiado, la escritura se rechaza: dos pestañas nunca se sobrescriben entre sí en silencio.
05
Qué se sella y por cuánto tiempo
Un mismo mecanismo cumple dos funciones: sella el inicio de sesión dentro de los tokens que guarda el chat, y la respuesta de un problema —la del actual y la del escrito por adelantado— dentro del perfil. Cada sello lleva su propósito, así que un token de acceso no puede hacerse pasar por un token de actualización, ni una respuesta sellada por ninguna otra cosa.
Un token, desglosado
mt1.a.kQ7fWx.5rJ0Yb2mQ8vK1nT4pXc9…
- mt1
- versión del formato
- propósito
- c código · a acceso · r actualización · d cliente · s state · k consentimiento · t respuesta de un problema
- id de la clave
- se toma de la propia clave, así que una rotación pasa inadvertida
- texto cifrado
- XChaCha20-Poly1305, un nonce aleatorio de 192 bits
No es un JWT: nadie más que el servicio lo lee, y de una cadena opaca no puede filtrarse ningún claim a un registro.
Vigencias, en escala logarítmica
- Código de autorización
60 s - Inicio de sesión en curso
10 min - Token de acceso
15 min - Token de actualización
30 días deslizantes, 90 como máximo - Clave de sellado
se rota cada 90 días - Clientes aprobados
180 días
Un servidor sin estado no puede marcar un código como usado, así que la ventana del código se mantiene pequeña. Tras una rotación, la clave anterior se sigue aceptando, de modo que ni un token ni una respuesta sellada se pierden cuando cambia la clave.