python-backend-mcp
Proyecto Curso — Control de Gastos con IA Generativa
Backend de control de gastos personales, construido durante el curso. Arquitectura: monolito modular + capas + Repository.
Reflexión (Paso 5)
¿Qué cambiaría si agrego un repository que guarde en JSON? En
services/gastos.py, nada — ninguna línea de_validar_gastoni deregistrar_gasto/listar_gastoscambia, porque el service depende de la abstracción "objeto conguardar,listar,total_por_categoria" (DIP), no de la implementación concreta en memoria. Solo se crearía un nuevo módulo (p. ej.app/repositories/gastos_json.py) con esos mismos tres métodos, y se pasaría comorepo=...al llamar al service (o se cambiaría el valor por defecto)."Voy a poner la validación del monto directo en el router": rompe SRP y la arquitectura en capas — el router debe recibir la solicitud, no decidir reglas de negocio. Si la validación vive en el router, queda duplicada o inconsistente en cualquier otro lugar que también llame a
registrar_gasto(otro endpoint, un tool de MCP en la Sesión 8, un test), y ya no se puede probar esa regla sin levantar el framework web. Al dejarla en_validar_gastodentro deservices/, se prueba con un simplepytest, sin mocks ni servidor, tal como hicimos hoy.
Cierre
"Hoy pude probar mi lógica de negocio sin levantar infraestructura real (sin base de datos, sin mocks de librerías), gracias a la inversión de dependencias (DIP): el service recibe el repository como parámetro, así que en los tests le inyecté un
RepositorioFalsocon el mismo contrato."
Sesión 7 — De la arquitectura al código
Repository en memoria reemplazado por SQLAlchemy + Alembic (SQLite), autenticación OAuth2 + JWT,
hashing de contraseñas con passlib/bcrypt, schemas Pydantic y routers REST protegidos.
Nota sobre passlib + bcrypt: bcrypt>=4.1 rompe con passlib==1.7.4 (lee un atributo interno
que ya no existe). Se fijó bcrypt<4.1 con uv add "bcrypt<4.1", tal como advierte el propio
enunciado, y hash_password/verify_password funcionan correctamente.
Hallazgo respecto al enunciado: el checkpoint del Paso 9 dice que tests/test_gastos.py de la
Sesión 6 "sigue pasando tal cual" — no es exacto. registrar_gasto/listar_gastos ahora llaman a
repo.total_por_categoria(db, usuario_id, categoria), repo.guardar(db, usuario_id, ...) y
repo.listar(db, usuario_id, skip, limit), con nuevos parámetros db y usuario_id. Se actualizó
RepositorioFalso (y las llamadas en los 4 tests) a esa misma firma — pasando None/1 como
placeholders que el doble ignora — para que sigan cumpliendo el mismo contrato que el repository
real, sin usar unittest.mock. _validar_gasto en sí no cambió ni una línea, que es la parte que
el enunciado sí cumple.
Reflexión (Paso 12)
¿Qué archivo tocaría para migrar SQLite → Postgres? Solo
DATABASE_URLen.env(y el driver enpyproject.toml, p. ej.psycopg).app/database.pyno cambiaría de código (usasettings.database_urlgenéricamente), salvo quitarconnect_args={"check_same_thread": False}, que es específico de SQLite. NO tocaríamodels/,schemas/,services/,repositories/nirouters/— todos hablan en términos de sesiones de SQLAlchemy, no de SQLite.¿Podría alguien ver gastos de otro usuario pasando
usuario_iden la URL? No, con el código de hoy no existe esa vía:GET /gastos/no leeusuario_idde la URL ni del query string en ningún punto — lo obtiene siempre deusuario_actual.id, que sale del JWT verificado enget_current_user(app/dependencies.py). Un atacante tendría que falsificar un token válido firmado con elSECRET_KEYdel servidor, no simplemente cambiar un parámetro en la URL.
Cierre
"Si mañana un atacante consigue mi archivo
.env, podría firmar tokens JWT válidos para cualquier usuario (tiene elSECRET_KEY) y leerDATABASE_URL, pero NO podría recuperar las contraseñas originales de los usuarios, porque solo se guardahashed_password(hash de bcrypt con salt, de un solo sentido) — nunca la contraseña en texto plano."
Sesión 8 — Del backend a los agentes
app/mcp/ deja de estar vacía: servidor MCP (app/mcp/server.py) con dos tools —
registrar_gasto y listar_gastos (app/mcp/tools/gastos.py) — que llaman directo a
app/services/gastos.py, el mismo que usa el router REST. services/gastos.py y
repositories/gastos.py no cambiaron ni una línea desde la Sesión 7 (verificado con
git diff --stat 507058a -- app/services/gastos.py app/repositories/gastos.py, sin salida).
Dependencia: uv add "mcp[cli]<2". El SDK instala por defecto mcp 2.x, que renombró
FastMCP a MCPServer y no expone mcp.server.fastmcp — el mismo tipo de incompatibilidad de
versión que bcrypt/passlib en la Sesión 7. Se fijó <2 para usar el código del enunciado tal cual.
Dos bugs reales encontrados y corregidos, reproducidos con el comando exacto del enunciado
(uv run mcp dev app/mcp/server.py):
Doble instancia de
FastMCP. El patrón del enunciado (tools/gastos.pyhacefrom app.mcp.server import mcp) rompe cuando el CLI demcpcargaserver.pycomo script standalone: ese import de vuelta por la ruta del paquete (app.mcp.server) re-ejecuta el archivo y crea una segunda instancia deFastMCP, distinta de la que el CLI corre. Los tools quedan registrados en la instancia "fantasma"; el cliente velist_tools()vacío yUnknown toolal intentar llamarlos. Verificado conid(): dos objetosFastMCPdistintos en memoria. Fix aplicado:tools/gastos.pyexponeregister(mcp)en vez de importarmcp;server.pycrea la instancia y llamagastos.register(mcp)explícitamente. Ningún archivo se reimporta a sí mismo por una ruta distinta.ModuleNotFoundError: No module named 'app'. El ejecutablemcp(entry-point de consola en.venv/bin/) no agrega la raíz del proyecto asys.path, a diferencia depython -m app.main. El proyecto no está declarado como paquete instalable, así queappno es importable fuera del directorio de trabajo. Fix: anteponerPYTHONPATH=.al comando, ej.PYTHONPATH=. uv run mcp dev app/mcp/server.py.
Verificación e2e real (sin mocks, protocolo MCP por stdio): un cliente ClientSession de la
SDK, conectado al servidor vía uv run mcp run app/mcp/server.py con PYTHONPATH=., confirmó los
4 pasos del enunciado: list_tools() devuelve los 2 tools con sus descripciones;
registrar_gasto("Almuerzo", 12.50, "comida") crea el gasto; listar_gastos() lo devuelve;
registrar_gasto con categoria="inventada" responde {"error": "'inventada' no es una categoría válida"} sin stack trace.
Reflexión
_obtener_o_crear_usuario_democon varios usuarios reales: se rompería primero el aislamiento de datos — todos los clientes MCP compartirían el mismousuario_id(el del email fijodemo@curso.com), así que cualquiera vería y modificaría los gastos de cualquier otro. Es exactamente el problema que el JWT resuelve en REST desde la Sesión 7; en MCP falta el equivalente (identidad propagada por el transporte/cliente).¿Qué habría estado mal en la Sesión 6 para tener que tocar
services/gastos.pyhoy? Queregistrar_gasto/listar_gastosdependieran directamente deapp.repositories.gastos(import fijo) en vez de recibirrepocomo parámetro (DIP), o que la validación de negocio (_validar_gasto) viviera en el router/repository en lugar de enservices/. Con cualquiera de los dos, el tool de MCP no habría podido reutilizar la lógica sin copiarla o sin acoplarse a FastAPI.
Cierre
"Hoy construí una tercera puerta de entrada al mismo backend. Lo que NO tuve que duplicar fue la lógica de negocio (
_validar_gasto,registrar_gasto,listar_gastos), y eso fue posible gracias a DIP y a la separación en capas (decisión que tomamos en la Sesión 6)."
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmurillov1/python-backend-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server