lanzakit-tester 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lanzakit_tester-0.1.0/.gitignore +155 -0
- lanzakit_tester-0.1.0/CLAUDE.md +95 -0
- lanzakit_tester-0.1.0/CONTRIBUTING.md +51 -0
- lanzakit_tester-0.1.0/LICENSE +21 -0
- lanzakit_tester-0.1.0/PKG-INFO +179 -0
- lanzakit_tester-0.1.0/README.md +149 -0
- lanzakit_tester-0.1.0/examples/hola_claude.py +46 -0
- lanzakit_tester-0.1.0/examples/test_hola_claude.py +157 -0
- lanzakit_tester-0.1.0/pyproject.toml +45 -0
- lanzakit_tester-0.1.0/requirements-dev.txt +3 -0
- lanzakit_tester-0.1.0/src/lanzakit_tester/__init__.py +14 -0
- lanzakit_tester-0.1.0/src/lanzakit_tester/cli.py +25 -0
- lanzakit_tester-0.1.0/src/lanzakit_tester/server.py +422 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
lerna-debug.log*
|
|
8
|
+
|
|
9
|
+
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
10
|
+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
11
|
+
|
|
12
|
+
# Runtime data
|
|
13
|
+
pids
|
|
14
|
+
*.pid
|
|
15
|
+
*.seed
|
|
16
|
+
*.pid.lock
|
|
17
|
+
|
|
18
|
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
19
|
+
lib-cov
|
|
20
|
+
|
|
21
|
+
# Coverage directory used by tools like istanbul
|
|
22
|
+
coverage
|
|
23
|
+
*.lcov
|
|
24
|
+
|
|
25
|
+
# nyc test coverage
|
|
26
|
+
.nyc_output
|
|
27
|
+
|
|
28
|
+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
29
|
+
.grunt
|
|
30
|
+
|
|
31
|
+
# Bower dependency directory (https://bower.io/)
|
|
32
|
+
bower_components
|
|
33
|
+
|
|
34
|
+
# node-waf configuration
|
|
35
|
+
.lock-wscript
|
|
36
|
+
|
|
37
|
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
38
|
+
build/Release
|
|
39
|
+
|
|
40
|
+
# Dependency directories
|
|
41
|
+
node_modules/
|
|
42
|
+
jspm_packages/
|
|
43
|
+
|
|
44
|
+
# Snowpack dependency directory (https://snowpack.dev/)
|
|
45
|
+
web_modules/
|
|
46
|
+
|
|
47
|
+
# TypeScript cache
|
|
48
|
+
*.tsbuildinfo
|
|
49
|
+
|
|
50
|
+
# Optional npm cache directory
|
|
51
|
+
.npm
|
|
52
|
+
|
|
53
|
+
# Optional eslint cache
|
|
54
|
+
.eslintcache
|
|
55
|
+
|
|
56
|
+
# Optional stylelint cache
|
|
57
|
+
.stylelintcache
|
|
58
|
+
|
|
59
|
+
# Optional REPL history
|
|
60
|
+
.node_repl_history
|
|
61
|
+
|
|
62
|
+
# Output of 'npm pack'
|
|
63
|
+
*.tgz
|
|
64
|
+
|
|
65
|
+
# Yarn Integrity file
|
|
66
|
+
.yarn-integrity
|
|
67
|
+
|
|
68
|
+
# dotenv environment variable files
|
|
69
|
+
.env
|
|
70
|
+
.env.*
|
|
71
|
+
!.env.example
|
|
72
|
+
|
|
73
|
+
# parcel-bundler cache (https://parceljs.org/)
|
|
74
|
+
.cache
|
|
75
|
+
.parcel-cache
|
|
76
|
+
|
|
77
|
+
# Next.js build output
|
|
78
|
+
.next
|
|
79
|
+
out
|
|
80
|
+
|
|
81
|
+
# Nuxt.js build / generate output
|
|
82
|
+
.nuxt
|
|
83
|
+
dist
|
|
84
|
+
.output
|
|
85
|
+
|
|
86
|
+
# Gatsby files
|
|
87
|
+
.cache/
|
|
88
|
+
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
89
|
+
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
90
|
+
# public
|
|
91
|
+
|
|
92
|
+
# vuepress build output
|
|
93
|
+
.vuepress/dist
|
|
94
|
+
|
|
95
|
+
# vuepress v2.x temp directory
|
|
96
|
+
.temp
|
|
97
|
+
|
|
98
|
+
# Sveltekit cache directory
|
|
99
|
+
.svelte-kit/
|
|
100
|
+
|
|
101
|
+
# vitepress build output
|
|
102
|
+
**/.vitepress/dist
|
|
103
|
+
|
|
104
|
+
# vitepress cache directory
|
|
105
|
+
**/.vitepress/cache
|
|
106
|
+
|
|
107
|
+
# Docusaurus cache and generated files
|
|
108
|
+
.docusaurus
|
|
109
|
+
|
|
110
|
+
# Serverless directories
|
|
111
|
+
.serverless/
|
|
112
|
+
|
|
113
|
+
# FuseBox cache
|
|
114
|
+
.fusebox/
|
|
115
|
+
|
|
116
|
+
# DynamoDB Local files
|
|
117
|
+
.dynamodb/
|
|
118
|
+
|
|
119
|
+
# Firebase cache directory
|
|
120
|
+
.firebase/
|
|
121
|
+
|
|
122
|
+
# TernJS port file
|
|
123
|
+
.tern-port
|
|
124
|
+
|
|
125
|
+
# Stores VSCode versions used for testing VSCode extensions
|
|
126
|
+
.vscode-test
|
|
127
|
+
|
|
128
|
+
# pnpm
|
|
129
|
+
.pnpm-store
|
|
130
|
+
|
|
131
|
+
# yarn v3
|
|
132
|
+
.pnp.*
|
|
133
|
+
.yarn/*
|
|
134
|
+
!.yarn/patches
|
|
135
|
+
!.yarn/plugins
|
|
136
|
+
!.yarn/releases
|
|
137
|
+
!.yarn/sdks
|
|
138
|
+
!.yarn/versions
|
|
139
|
+
|
|
140
|
+
# Vite files
|
|
141
|
+
vite.config.js.timestamp-*
|
|
142
|
+
vite.config.ts.timestamp-*
|
|
143
|
+
.vite/
|
|
144
|
+
# Python
|
|
145
|
+
venv/
|
|
146
|
+
__pycache__/
|
|
147
|
+
*.pyc
|
|
148
|
+
.pytest_cache/
|
|
149
|
+
|
|
150
|
+
# Secrets
|
|
151
|
+
.env
|
|
152
|
+
|
|
153
|
+
# IDE
|
|
154
|
+
.vscode/
|
|
155
|
+
.idea/
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Lanzakit Tester
|
|
2
|
+
|
|
3
|
+
Proyecto: MCP server en Python que genera y valida tests pytest con IA.
|
|
4
|
+
|
|
5
|
+
## Estado actual (noviembre 2026)
|
|
6
|
+
|
|
7
|
+
Paquete Python instalable (`pip install lanzakit-tester`), versión 0.1.0, aún sin publicar en PyPI.
|
|
8
|
+
Estructura `src/lanzakit_tester/` con build backend hatchling y CLI `lanzakit-tester`.
|
|
9
|
+
|
|
10
|
+
Producto funcional con 3 tools:
|
|
11
|
+
- `analyze_python_file`: analiza estructura del código con AST
|
|
12
|
+
- `generate_tests_for_file`: genera tests con Claude API
|
|
13
|
+
- `generate_and_validate_tests`: genera + valida sintaxis + ejecuta pytest
|
|
14
|
+
|
|
15
|
+
Loop end-to-end funciona. 8 tests generados y 8 passed en pruebas reales.
|
|
16
|
+
|
|
17
|
+
## Stack técnico
|
|
18
|
+
|
|
19
|
+
- Python 3.14
|
|
20
|
+
- MCP SDK v2 (`from mcp.server.mcpserver import MCPServer`)
|
|
21
|
+
- Claude API (modelo: `claude-sonnet-5`)
|
|
22
|
+
- pytest para validación
|
|
23
|
+
- uv como gestor de paquetes
|
|
24
|
+
- venv en `.venv/`
|
|
25
|
+
|
|
26
|
+
## Comandos clave
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Activar entorno
|
|
30
|
+
source .venv/bin/activate
|
|
31
|
+
|
|
32
|
+
# Instalar en modo editable (con deps de desarrollo)
|
|
33
|
+
uv pip install -e ".[dev]"
|
|
34
|
+
|
|
35
|
+
# Verificar CLI
|
|
36
|
+
lanzakit-tester --version
|
|
37
|
+
|
|
38
|
+
# Probar generación de tests
|
|
39
|
+
python -c "from lanzakit_tester import generate_and_validate_tests; print(generate_and_validate_tests('/ruta/archivo.py')['summary'])"
|
|
40
|
+
|
|
41
|
+
# Correr tests manualmente
|
|
42
|
+
pytest examples -v
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Arquitectura
|
|
46
|
+
|
|
47
|
+
- `pyproject.toml`: fuente de verdad de metadata y dependencias (hatchling)
|
|
48
|
+
- `src/lanzakit_tester/server.py`: MCP server con las 3 tools, constante `MODEL`, `run()`
|
|
49
|
+
- `src/lanzakit_tester/cli.py`: entrypoint argparse (`--version`; sin args arranca MCP por stdio)
|
|
50
|
+
- `src/lanzakit_tester/__init__.py`: expone las 3 tools y `__version__`
|
|
51
|
+
- `examples/`: `hola_claude.py` (hola mundo con Claude API) y su test; no son parte del paquete
|
|
52
|
+
- `.env`: contiene ANTHROPIC_API_KEY (nunca commitear)
|
|
53
|
+
|
|
54
|
+
## Decisiones tomadas
|
|
55
|
+
|
|
56
|
+
- Modelo: Claude Sonnet 5 (fijo por ahora), definido como constante `MODEL` en `src/lanzakit_tester/server.py`
|
|
57
|
+
- Versión única en `pyproject.toml`; se lee con `importlib.metadata` y se pasa a `MCPServer(version=...)`
|
|
58
|
+
- `max_tokens`: 16384 con thinking desactivado
|
|
59
|
+
- Prompt engineering con delimitadores XML
|
|
60
|
+
- `_clean_code_output()` remueve markdown fences
|
|
61
|
+
- `_run_pytest` usa `sys.executable -m pytest` para encontrar pytest desde cualquier contexto
|
|
62
|
+
- Thinking desactivado para ahorrar tokens (reactivable cuando se quiera)
|
|
63
|
+
|
|
64
|
+
## Próximos pasos
|
|
65
|
+
|
|
66
|
+
- README público para GitHub
|
|
67
|
+
- Publicar en PyPI como `lanzakit-tester` (el empaquetado ya está listo)
|
|
68
|
+
- Landing en lanzakit.dev con waitlist
|
|
69
|
+
- Lanzamiento en X/LinkedIn + Product Hunt
|
|
70
|
+
|
|
71
|
+
## Deuda técnica menor
|
|
72
|
+
|
|
73
|
+
No se resuelve antes del lanzamiento (perfeccionismo mata lanzamientos):
|
|
74
|
+
|
|
75
|
+
- `requirements-dev.txt` es redundante con `pip install -e ".[dev]"`
|
|
76
|
+
- `CONTRIBUTING.md` no menciona `pip install -e ".[dev]"`
|
|
77
|
+
|
|
78
|
+
## Contexto personal
|
|
79
|
+
|
|
80
|
+
Ingeniera full stack en Jalasoft (La Paz, Bolivia).
|
|
81
|
+
Prefiere respuestas concisas y directas.
|
|
82
|
+
Comunica en español.
|
|
83
|
+
|
|
84
|
+
## Reglas del proyecto
|
|
85
|
+
|
|
86
|
+
- Nunca commitear `.env`
|
|
87
|
+
- Tests en español (comentarios) pero nombres de funciones en inglés
|
|
88
|
+
- Mantener imports ordenados: stdlib → third party → local
|
|
89
|
+
- Modelo API siempre desde constante/config, nunca hardcoded en varias partes
|
|
90
|
+
|
|
91
|
+
## Pendientes conocidos
|
|
92
|
+
|
|
93
|
+
- Renombrar repo de `starter-kit` a `lanzakit-tester` antes del lanzamiento público
|
|
94
|
+
- Reservar handle @lanzakit en X/Twitter
|
|
95
|
+
- Crear archivos de badges reales cuando se publique en PyPI
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Contribuir a Lanzakit Tester
|
|
2
|
+
|
|
3
|
+
¡Gracias por tu interés! Toda contribución es bienvenida: bugs, ideas, docs y código.
|
|
4
|
+
|
|
5
|
+
## Reportar un issue
|
|
6
|
+
|
|
7
|
+
1. Busca primero si ya existe un issue similar.
|
|
8
|
+
2. Abre uno nuevo e incluye:
|
|
9
|
+
- Qué esperabas que pasara y qué pasó realmente.
|
|
10
|
+
- Pasos para reproducirlo (idealmente con un archivo `.py` mínimo).
|
|
11
|
+
- Versión de Python y sistema operativo.
|
|
12
|
+
- Mensajes de error completos.
|
|
13
|
+
|
|
14
|
+
Nunca pegues tu `ANTHROPIC_API_KEY` ni el contenido de tu `.env`.
|
|
15
|
+
|
|
16
|
+
## Proponer una feature
|
|
17
|
+
|
|
18
|
+
Abre un issue describiendo el problema que quieres resolver y tu propuesta.
|
|
19
|
+
Conviene acordar el enfoque antes de escribir código, así evitamos trabajo perdido.
|
|
20
|
+
|
|
21
|
+
## Hacer un Pull Request
|
|
22
|
+
|
|
23
|
+
1. Haz fork del repositorio.
|
|
24
|
+
2. Crea una rama desde `main`: `git checkout -b feat/mi-cambio`
|
|
25
|
+
3. Haz tus cambios y commits pequeños y claros.
|
|
26
|
+
4. Verifica que los tests pasen: `pytest -v`
|
|
27
|
+
5. Haz push a tu fork y abre el PR explicando qué cambia y por qué.
|
|
28
|
+
|
|
29
|
+
Mensajes de commit sugeridos: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`.
|
|
30
|
+
|
|
31
|
+
## Estilo de código
|
|
32
|
+
|
|
33
|
+
- Sigue [PEP 8](https://peps.python.org/pep-0008/).
|
|
34
|
+
- Formatea con [black](https://black.readthedocs.io/) y ordena imports con [isort](https://pycqa.github.io/isort/):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
black .
|
|
38
|
+
isort .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- Orden de imports: stdlib → third party → local.
|
|
42
|
+
|
|
43
|
+
## Convenciones del proyecto
|
|
44
|
+
|
|
45
|
+
- Comentarios y docstrings en español; nombres de funciones y variables en inglés.
|
|
46
|
+
- El modelo de la API se define en una sola constante/config, nunca hardcoded en varios lugares.
|
|
47
|
+
- Nunca commitees `.env` ni credenciales.
|
|
48
|
+
|
|
49
|
+
## Licencia
|
|
50
|
+
|
|
51
|
+
Al contribuir aceptas que tu aporte se distribuya bajo la licencia [MIT](LICENSE) del proyecto.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Raquel Martinez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lanzakit-tester
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server that generates and validates pytest tests using Claude.
|
|
5
|
+
Project-URL: Homepage, https://github.com/lanzakit/starter-kit
|
|
6
|
+
Project-URL: Repository, https://github.com/lanzakit/starter-kit
|
|
7
|
+
Project-URL: Issues, https://github.com/lanzakit/starter-kit/issues
|
|
8
|
+
Author: Raquel Martinez
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,anthropic,claude,mcp,pytest,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: anthropic>=2.0.0
|
|
23
|
+
Requires-Dist: mcp[cli]>=2.0.0
|
|
24
|
+
Requires-Dist: pytest>=8.0.0
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: black; extra == 'dev'
|
|
28
|
+
Requires-Dist: isort; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Lanzakit Tester
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+

|
|
35
|
+
<!-- TODO: agregar badge PyPI después del primer release -->
|
|
36
|
+
|
|
37
|
+
**Tests de pytest generados por IA, y verificados antes de que los veas.**
|
|
38
|
+
|
|
39
|
+
Lanzakit Tester es un MCP server que lee tu código Python, genera tests con Claude y los ejecuta para confirmar que compilan y pasan. Dejas de revisar tests que ni siquiera corren.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- 🧪 **Generación de tests con Claude**: analiza tu código y produce tests pytest listos para usar.
|
|
44
|
+
- ✅ **Validación automática**: verifica la sintaxis y ejecuta pytest antes de entregarte el resultado.
|
|
45
|
+
- 🔌 **MCP nativo**: funciona con Claude Desktop y cualquier cliente compatible con MCP.
|
|
46
|
+
- 🌳 **Análisis con AST**: entiende funciones, clases y firmas antes de armar el prompt.
|
|
47
|
+
- 🇪🇸 **Español-first**: comentarios de los tests y documentación en español.
|
|
48
|
+
- 🔓 **Open source**: licencia MIT, sin lock-in.
|
|
49
|
+
|
|
50
|
+
## Requisitos
|
|
51
|
+
|
|
52
|
+
- Python 3.11+
|
|
53
|
+
- Una API key de Anthropic (`ANTHROPIC_API_KEY`)
|
|
54
|
+
- Acceso al modelo `claude-sonnet-5`
|
|
55
|
+
|
|
56
|
+
## Instalación
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install lanzakit-tester
|
|
60
|
+
echo "ANTHROPIC_API_KEY=tu_api_key" > .env
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> El archivo `.env` contiene tu API key. Nunca lo subas a git.
|
|
64
|
+
|
|
65
|
+
### Opción A: Vía Claude Desktop (MCP config)
|
|
66
|
+
|
|
67
|
+
Agrega el server a tu `claude_desktop_config.json`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"lanzakit-tester": {
|
|
73
|
+
"command": "lanzakit-tester"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Reinicia Claude Desktop y las 3 tools aparecerán disponibles.
|
|
80
|
+
|
|
81
|
+
### Opción B: Vía Python
|
|
82
|
+
|
|
83
|
+
Sin cliente MCP, puedes invocar las tools directamente desde Python:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python -c "from lanzakit_tester import generate_and_validate_tests; print(generate_and_validate_tests('/ruta/a/tu_archivo.py')['summary'])"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Para contribuir
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/lanzakit/starter-kit.git
|
|
93
|
+
cd starter-kit
|
|
94
|
+
uv venv && source .venv/bin/activate
|
|
95
|
+
uv pip install -e ".[dev]"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Uso
|
|
99
|
+
|
|
100
|
+
Tienes un archivo `hola_claude.py` (ver `examples/hola_claude.py`, versión simplificada):
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import os
|
|
104
|
+
|
|
105
|
+
from anthropic import Anthropic
|
|
106
|
+
from dotenv import load_dotenv
|
|
107
|
+
|
|
108
|
+
load_dotenv()
|
|
109
|
+
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def saludar():
|
|
113
|
+
message = client.messages.create(
|
|
114
|
+
model="claude-sonnet-5",
|
|
115
|
+
max_tokens=512,
|
|
116
|
+
messages=[{"role": "user", "content": "Hola Claude"}]
|
|
117
|
+
)
|
|
118
|
+
print(message.content[0].text)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Desde Claude Desktop, pídele:
|
|
122
|
+
|
|
123
|
+
> Genera y valida tests para `/ruta/a/tu_archivo.py`
|
|
124
|
+
|
|
125
|
+
Claude llama a la tool `generate_and_validate_tests` y obtienes un resumen como este:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
✅ 8 tests generados y pasaron. Guardado en: /ruta/a/test_tu_archivo.py
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
El archivo `test_tu_archivo.py` queda junto a tu código, listo para correr con `pytest`.
|
|
132
|
+
|
|
133
|
+
### Tools disponibles
|
|
134
|
+
|
|
135
|
+
| Tool | Qué hace |
|
|
136
|
+
|------|----------|
|
|
137
|
+
| `analyze_python_file` | Analiza la estructura del código con AST. |
|
|
138
|
+
| `generate_tests_for_file` | Genera tests pytest con Claude. |
|
|
139
|
+
| `generate_and_validate_tests` | Genera, valida sintaxis, guarda y ejecuta pytest. |
|
|
140
|
+
|
|
141
|
+
## Cómo funciona
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
código .py
|
|
145
|
+
│
|
|
146
|
+
▼
|
|
147
|
+
análisis AST ──► prompt a Claude ──► tests generados
|
|
148
|
+
│
|
|
149
|
+
▼
|
|
150
|
+
resultado ◄── ejecución pytest ◄── validación sintáctica
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
1. Lee tu archivo y extrae su estructura con AST.
|
|
154
|
+
2. Arma un prompt con el código y su contexto, y se lo envía a Claude (`claude-sonnet-5`).
|
|
155
|
+
3. Limpia la respuesta (por ejemplo, quita markdown fences).
|
|
156
|
+
4. Valida que los tests tengan sintaxis Python correcta.
|
|
157
|
+
5. Guarda el archivo y ejecuta pytest.
|
|
158
|
+
6. Devuelve un resumen: cuántos pasaron, fallaron o dieron error.
|
|
159
|
+
|
|
160
|
+
## Roadmap
|
|
161
|
+
|
|
162
|
+
- [x] 3 tools funcionales
|
|
163
|
+
- [x] Validación end-to-end
|
|
164
|
+
- [x] Empaquetado como paquete pip
|
|
165
|
+
- [ ] Publicación en PyPI
|
|
166
|
+
- [ ] Landing en lanzakit.dev
|
|
167
|
+
- [ ] Soporte para más frameworks de testing
|
|
168
|
+
|
|
169
|
+
## Contribuir
|
|
170
|
+
|
|
171
|
+
¿Encontraste un bug o tienes una idea? Abre un [issue](https://github.com/lanzakit/starter-kit/issues) o envía un PR. Lee [CONTRIBUTING.md](CONTRIBUTING.md) para conocer el flujo de trabajo.
|
|
172
|
+
|
|
173
|
+
## Licencia
|
|
174
|
+
|
|
175
|
+
Distribuido bajo licencia MIT. Consulta el archivo [LICENSE](LICENSE).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
[@lanzakit](https://x.com/lanzakit) · Construido en Bolivia 🇧🇴 para devs hispanohablantes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Lanzakit Tester
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
<!-- TODO: agregar badge PyPI después del primer release -->
|
|
6
|
+
|
|
7
|
+
**Tests de pytest generados por IA, y verificados antes de que los veas.**
|
|
8
|
+
|
|
9
|
+
Lanzakit Tester es un MCP server que lee tu código Python, genera tests con Claude y los ejecuta para confirmar que compilan y pasan. Dejas de revisar tests que ni siquiera corren.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🧪 **Generación de tests con Claude**: analiza tu código y produce tests pytest listos para usar.
|
|
14
|
+
- ✅ **Validación automática**: verifica la sintaxis y ejecuta pytest antes de entregarte el resultado.
|
|
15
|
+
- 🔌 **MCP nativo**: funciona con Claude Desktop y cualquier cliente compatible con MCP.
|
|
16
|
+
- 🌳 **Análisis con AST**: entiende funciones, clases y firmas antes de armar el prompt.
|
|
17
|
+
- 🇪🇸 **Español-first**: comentarios de los tests y documentación en español.
|
|
18
|
+
- 🔓 **Open source**: licencia MIT, sin lock-in.
|
|
19
|
+
|
|
20
|
+
## Requisitos
|
|
21
|
+
|
|
22
|
+
- Python 3.11+
|
|
23
|
+
- Una API key de Anthropic (`ANTHROPIC_API_KEY`)
|
|
24
|
+
- Acceso al modelo `claude-sonnet-5`
|
|
25
|
+
|
|
26
|
+
## Instalación
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install lanzakit-tester
|
|
30
|
+
echo "ANTHROPIC_API_KEY=tu_api_key" > .env
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> El archivo `.env` contiene tu API key. Nunca lo subas a git.
|
|
34
|
+
|
|
35
|
+
### Opción A: Vía Claude Desktop (MCP config)
|
|
36
|
+
|
|
37
|
+
Agrega el server a tu `claude_desktop_config.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"lanzakit-tester": {
|
|
43
|
+
"command": "lanzakit-tester"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Reinicia Claude Desktop y las 3 tools aparecerán disponibles.
|
|
50
|
+
|
|
51
|
+
### Opción B: Vía Python
|
|
52
|
+
|
|
53
|
+
Sin cliente MCP, puedes invocar las tools directamente desde Python:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python -c "from lanzakit_tester import generate_and_validate_tests; print(generate_and_validate_tests('/ruta/a/tu_archivo.py')['summary'])"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Para contribuir
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/lanzakit/starter-kit.git
|
|
63
|
+
cd starter-kit
|
|
64
|
+
uv venv && source .venv/bin/activate
|
|
65
|
+
uv pip install -e ".[dev]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Uso
|
|
69
|
+
|
|
70
|
+
Tienes un archivo `hola_claude.py` (ver `examples/hola_claude.py`, versión simplificada):
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import os
|
|
74
|
+
|
|
75
|
+
from anthropic import Anthropic
|
|
76
|
+
from dotenv import load_dotenv
|
|
77
|
+
|
|
78
|
+
load_dotenv()
|
|
79
|
+
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def saludar():
|
|
83
|
+
message = client.messages.create(
|
|
84
|
+
model="claude-sonnet-5",
|
|
85
|
+
max_tokens=512,
|
|
86
|
+
messages=[{"role": "user", "content": "Hola Claude"}]
|
|
87
|
+
)
|
|
88
|
+
print(message.content[0].text)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Desde Claude Desktop, pídele:
|
|
92
|
+
|
|
93
|
+
> Genera y valida tests para `/ruta/a/tu_archivo.py`
|
|
94
|
+
|
|
95
|
+
Claude llama a la tool `generate_and_validate_tests` y obtienes un resumen como este:
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
✅ 8 tests generados y pasaron. Guardado en: /ruta/a/test_tu_archivo.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
El archivo `test_tu_archivo.py` queda junto a tu código, listo para correr con `pytest`.
|
|
102
|
+
|
|
103
|
+
### Tools disponibles
|
|
104
|
+
|
|
105
|
+
| Tool | Qué hace |
|
|
106
|
+
|------|----------|
|
|
107
|
+
| `analyze_python_file` | Analiza la estructura del código con AST. |
|
|
108
|
+
| `generate_tests_for_file` | Genera tests pytest con Claude. |
|
|
109
|
+
| `generate_and_validate_tests` | Genera, valida sintaxis, guarda y ejecuta pytest. |
|
|
110
|
+
|
|
111
|
+
## Cómo funciona
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
código .py
|
|
115
|
+
│
|
|
116
|
+
▼
|
|
117
|
+
análisis AST ──► prompt a Claude ──► tests generados
|
|
118
|
+
│
|
|
119
|
+
▼
|
|
120
|
+
resultado ◄── ejecución pytest ◄── validación sintáctica
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
1. Lee tu archivo y extrae su estructura con AST.
|
|
124
|
+
2. Arma un prompt con el código y su contexto, y se lo envía a Claude (`claude-sonnet-5`).
|
|
125
|
+
3. Limpia la respuesta (por ejemplo, quita markdown fences).
|
|
126
|
+
4. Valida que los tests tengan sintaxis Python correcta.
|
|
127
|
+
5. Guarda el archivo y ejecuta pytest.
|
|
128
|
+
6. Devuelve un resumen: cuántos pasaron, fallaron o dieron error.
|
|
129
|
+
|
|
130
|
+
## Roadmap
|
|
131
|
+
|
|
132
|
+
- [x] 3 tools funcionales
|
|
133
|
+
- [x] Validación end-to-end
|
|
134
|
+
- [x] Empaquetado como paquete pip
|
|
135
|
+
- [ ] Publicación en PyPI
|
|
136
|
+
- [ ] Landing en lanzakit.dev
|
|
137
|
+
- [ ] Soporte para más frameworks de testing
|
|
138
|
+
|
|
139
|
+
## Contribuir
|
|
140
|
+
|
|
141
|
+
¿Encontraste un bug o tienes una idea? Abre un [issue](https://github.com/lanzakit/starter-kit/issues) o envía un PR. Lee [CONTRIBUTING.md](CONTRIBUTING.md) para conocer el flujo de trabajo.
|
|
142
|
+
|
|
143
|
+
## Licencia
|
|
144
|
+
|
|
145
|
+
Distribuido bajo licencia MIT. Consulta el archivo [LICENSE](LICENSE).
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
[@lanzakit](https://x.com/lanzakit) · Construido en Bolivia 🇧🇴 para devs hispanohablantes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Primer script: saludar a Claude y verificar que la API funciona.
|
|
3
|
+
Este script es el 'Hola Mundo' del proyecto Lanzakit Tester.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from anthropic import Anthropic
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
# Cargar variables de entorno desde .env
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
# Inicializar cliente de Claude
|
|
13
|
+
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def saludar():
|
|
17
|
+
"""Envía un mensaje de prueba a Claude y muestra la respuesta."""
|
|
18
|
+
message = client.messages.create(
|
|
19
|
+
model="claude-sonnet-5",
|
|
20
|
+
max_tokens=512,
|
|
21
|
+
messages=[
|
|
22
|
+
{
|
|
23
|
+
"role": "user",
|
|
24
|
+
"content": (
|
|
25
|
+
"Hola Claude, soy Raquel y estoy construyendo "
|
|
26
|
+
"Lanzakit Tester, un agente de IA para generar tests "
|
|
27
|
+
"de Python. ¿Estás listo para ayudarme en este proyecto? "
|
|
28
|
+
"Responde en 2 líneas."
|
|
29
|
+
),
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
print("Respuesta de Claude:")
|
|
35
|
+
print("-" * 50)
|
|
36
|
+
print(message.content[0].text)
|
|
37
|
+
print("-" * 50)
|
|
38
|
+
print(
|
|
39
|
+
f"\nTokens usados: "
|
|
40
|
+
f"{message.usage.input_tokens} input + "
|
|
41
|
+
f"{message.usage.output_tokens} output"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
saludar()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests para hola_claude.py: verifica la lógica de saludar()
|
|
3
|
+
mockeando la frontera externa (API de Anthropic).
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# El módulo bajo prueba instancia Anthropic() al importarse (nivel módulo).
|
|
12
|
+
# Por eso mockeamos la clase Anthropic ANTES de importar hola_claude,
|
|
13
|
+
# para evitar llamadas reales o errores por falta de API key.
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def mock_anthropic_client():
|
|
16
|
+
"""Reemplaza la clase Anthropic por un mock antes de importar el módulo."""
|
|
17
|
+
with patch("anthropic.Anthropic") as mock_class:
|
|
18
|
+
mock_instance = MagicMock()
|
|
19
|
+
mock_class.return_value = mock_instance
|
|
20
|
+
yield mock_class, mock_instance
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def hola_claude_module(mock_anthropic_client, monkeypatch):
|
|
25
|
+
"""Importa (o recarga) hola_claude con Anthropic ya mockeado."""
|
|
26
|
+
# Aseguramos que exista una API key dummy para que os.getenv no sea None.
|
|
27
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "fake-key-for-tests")
|
|
28
|
+
|
|
29
|
+
import importlib
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
# Si el módulo ya fue importado por otro test, lo removemos para
|
|
33
|
+
# forzar una reimportación limpia con el mock activo.
|
|
34
|
+
if "hola_claude" in sys.modules:
|
|
35
|
+
del sys.modules["hola_claude"]
|
|
36
|
+
|
|
37
|
+
module = importlib.import_module("hola_claude")
|
|
38
|
+
importlib.reload(module)
|
|
39
|
+
return module
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_fake_message(text="Respuesta de prueba", input_tokens=10, output_tokens=5):
|
|
43
|
+
"""Construye un objeto mensaje falso con la forma que espera el código."""
|
|
44
|
+
fake_message = MagicMock()
|
|
45
|
+
fake_content_block = MagicMock()
|
|
46
|
+
fake_content_block.text = text
|
|
47
|
+
fake_message.content = [fake_content_block]
|
|
48
|
+
fake_message.usage.input_tokens = input_tokens
|
|
49
|
+
fake_message.usage.output_tokens = output_tokens
|
|
50
|
+
return fake_message
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_saludar_llama_a_messages_create_con_modelo_correcto(
|
|
54
|
+
hola_claude_module, mock_anthropic_client
|
|
55
|
+
):
|
|
56
|
+
"""Protege contra regresiones que cambien el modelo o rompan la llamada a la API."""
|
|
57
|
+
_, mock_instance = mock_anthropic_client
|
|
58
|
+
mock_instance.messages.create.return_value = _build_fake_message()
|
|
59
|
+
|
|
60
|
+
hola_claude_module.client = mock_instance # aseguramos el mock en el módulo
|
|
61
|
+
hola_claude_module.saludar()
|
|
62
|
+
|
|
63
|
+
mock_instance.messages.create.assert_called_once()
|
|
64
|
+
_, kwargs = mock_instance.messages.create.call_args
|
|
65
|
+
assert kwargs["model"] == "claude-sonnet-5"
|
|
66
|
+
assert kwargs["max_tokens"] == 512
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_saludar_envia_mensaje_con_rol_user_y_contenido_no_vacio(
|
|
70
|
+
hola_claude_module, mock_anthropic_client
|
|
71
|
+
):
|
|
72
|
+
"""Protege contra cambios accidentales en el payload enviado a Claude."""
|
|
73
|
+
_, mock_instance = mock_anthropic_client
|
|
74
|
+
mock_instance.messages.create.return_value = _build_fake_message()
|
|
75
|
+
|
|
76
|
+
hola_claude_module.client = mock_instance
|
|
77
|
+
hola_claude_module.saludar()
|
|
78
|
+
|
|
79
|
+
_, kwargs = mock_instance.messages.create.call_args
|
|
80
|
+
messages = kwargs["messages"]
|
|
81
|
+
assert len(messages) == 1
|
|
82
|
+
assert messages[0]["role"] == "user"
|
|
83
|
+
assert isinstance(messages[0]["content"], str)
|
|
84
|
+
assert len(messages[0]["content"]) > 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_saludar_imprime_respuesta_y_tokens(
|
|
88
|
+
hola_claude_module, mock_anthropic_client, capsys
|
|
89
|
+
):
|
|
90
|
+
"""Protege contra regresiones en el formato de salida: texto y conteo de tokens."""
|
|
91
|
+
_, mock_instance = mock_anthropic_client
|
|
92
|
+
mock_instance.messages.create.return_value = _build_fake_message(
|
|
93
|
+
text="Sí, estoy listo.", input_tokens=42, output_tokens=7
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
hola_claude_module.client = mock_instance
|
|
97
|
+
hola_claude_module.saludar()
|
|
98
|
+
|
|
99
|
+
captured = capsys.readouterr()
|
|
100
|
+
assert "Sí, estoy listo." in captured.out
|
|
101
|
+
assert "42 input" in captured.out
|
|
102
|
+
assert "7 output" in captured.out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.parametrize(
|
|
106
|
+
"input_tokens,output_tokens",
|
|
107
|
+
[
|
|
108
|
+
(0, 0), # caso límite: respuesta sin consumo de tokens reportado
|
|
109
|
+
(1, 1), # caso mínimo no nulo
|
|
110
|
+
(100000, 50000), # caso de valores grandes, detecta overflow o truncamiento
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
def test_saludar_formatea_distintos_valores_de_tokens(
|
|
114
|
+
hola_claude_module, mock_anthropic_client, capsys, input_tokens, output_tokens
|
|
115
|
+
):
|
|
116
|
+
"""Protege contra errores de formato al construir el string de tokens usados."""
|
|
117
|
+
_, mock_instance = mock_anthropic_client
|
|
118
|
+
mock_instance.messages.create.return_value = _build_fake_message(
|
|
119
|
+
input_tokens=input_tokens, output_tokens=output_tokens
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
hola_claude_module.client = mock_instance
|
|
123
|
+
hola_claude_module.saludar()
|
|
124
|
+
|
|
125
|
+
captured = capsys.readouterr()
|
|
126
|
+
assert f"{input_tokens} input" in captured.out
|
|
127
|
+
assert f"{output_tokens} output" in captured.out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_saludar_propaga_excepcion_si_api_falla(
|
|
131
|
+
hola_claude_module, mock_anthropic_client
|
|
132
|
+
):
|
|
133
|
+
"""Verifica que un error de la API no se silencia, sino que se propaga al llamador."""
|
|
134
|
+
_, mock_instance = mock_anthropic_client
|
|
135
|
+
mock_instance.messages.create.side_effect = RuntimeError("fallo de red simulado")
|
|
136
|
+
|
|
137
|
+
hola_claude_module.client = mock_instance
|
|
138
|
+
|
|
139
|
+
with pytest.raises(RuntimeError, match="fallo de red simulado"):
|
|
140
|
+
hola_claude_module.saludar()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_client_se_inicializa_con_api_key_desde_entorno(
|
|
144
|
+
mock_anthropic_client, monkeypatch
|
|
145
|
+
):
|
|
146
|
+
"""Protege contra regresiones donde la API key deje de leerse de la variable de entorno."""
|
|
147
|
+
mock_class, _ = mock_anthropic_client
|
|
148
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "clave-especifica-de-test")
|
|
149
|
+
|
|
150
|
+
import importlib
|
|
151
|
+
import sys
|
|
152
|
+
|
|
153
|
+
if "hola_claude" in sys.modules:
|
|
154
|
+
del sys.modules["hola_claude"]
|
|
155
|
+
importlib.import_module("hola_claude")
|
|
156
|
+
|
|
157
|
+
mock_class.assert_called_once_with(api_key="clave-especifica-de-test")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lanzakit-tester"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server that generates and validates pytest tests using Claude."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Raquel Martinez" }]
|
|
14
|
+
keywords = ["mcp", "pytest", "testing", "ai", "claude", "anthropic"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
"Topic :: Software Development :: Testing",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"anthropic>=2.0.0",
|
|
28
|
+
"python-dotenv>=1.0.0",
|
|
29
|
+
"mcp[cli]>=2.0.0",
|
|
30
|
+
"pytest>=8.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = ["black", "isort"]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/lanzakit/starter-kit"
|
|
38
|
+
Repository = "https://github.com/lanzakit/starter-kit"
|
|
39
|
+
Issues = "https://github.com/lanzakit/starter-kit/issues"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
lanzakit-tester = "lanzakit_tester.cli:main"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/lanzakit_tester"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Lanzakit Tester: genera y valida tests pytest con IA (MCP server)."""
|
|
2
|
+
from lanzakit_tester.server import (
|
|
3
|
+
__version__,
|
|
4
|
+
analyze_python_file,
|
|
5
|
+
generate_and_validate_tests,
|
|
6
|
+
generate_tests_for_file,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"__version__",
|
|
11
|
+
"analyze_python_file",
|
|
12
|
+
"generate_and_validate_tests",
|
|
13
|
+
"generate_tests_for_file",
|
|
14
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Entrypoint de línea de comandos de Lanzakit Tester."""
|
|
2
|
+
import argparse
|
|
3
|
+
|
|
4
|
+
from lanzakit_tester import __version__
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
prog="lanzakit-tester",
|
|
10
|
+
description="Lanzakit Tester: MCP server que genera y valida tests pytest con IA. "
|
|
11
|
+
"Sin argumentos, arranca el servidor MCP por stdio.",
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
15
|
+
)
|
|
16
|
+
parser.parse_args()
|
|
17
|
+
|
|
18
|
+
# Import diferido: evita inicializar el servidor si solo se pide --help/--version
|
|
19
|
+
from lanzakit_tester.server import run
|
|
20
|
+
|
|
21
|
+
run()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
main()
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lanzakit Tester - MCP Server
|
|
3
|
+
|
|
4
|
+
Agente de IA para generar y analizar tests de Python.
|
|
5
|
+
"""
|
|
6
|
+
import ast
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from anthropic import Anthropic
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
from mcp.server.mcpserver import MCPServer
|
|
16
|
+
|
|
17
|
+
load_dotenv()
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
__version__ = version("lanzakit-tester")
|
|
21
|
+
except PackageNotFoundError: # ejecutando desde el código fuente sin instalar
|
|
22
|
+
__version__ = "0.0.0+unknown"
|
|
23
|
+
|
|
24
|
+
# Modelo de Claude usado para generar los tests (único punto de configuración)
|
|
25
|
+
MODEL = "claude-sonnet-5"
|
|
26
|
+
|
|
27
|
+
anthropic_client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _clean_code_output(text: str) -> str:
|
|
31
|
+
"""Remueve markdown fences si Claude los agrega a pesar del prompt."""
|
|
32
|
+
text = text.strip()
|
|
33
|
+
if text.startswith("```python"):
|
|
34
|
+
text = text[9:].lstrip()
|
|
35
|
+
elif text.startswith("```"):
|
|
36
|
+
text = text[3:].lstrip()
|
|
37
|
+
if text.endswith("```"):
|
|
38
|
+
text = text[:-3].rstrip()
|
|
39
|
+
return text
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Crear el servidor MCP
|
|
43
|
+
mcp = MCPServer("lanzakit-tester", version=__version__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@mcp.tool()
|
|
47
|
+
def analyze_python_file(file_path: str) -> dict:
|
|
48
|
+
"""
|
|
49
|
+
Analiza un archivo Python y devuelve información estructural.
|
|
50
|
+
|
|
51
|
+
Lee el archivo, lo parsea con AST (Abstract Syntax Tree) y extrae:
|
|
52
|
+
- Nombre del archivo
|
|
53
|
+
- Número de líneas totales
|
|
54
|
+
- Lista de clases definidas
|
|
55
|
+
- Lista de funciones definidas (incluye métodos de clases)
|
|
56
|
+
- Imports utilizados
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
file_path: Ruta absoluta al archivo .py a analizar
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Diccionario con el análisis estructural del archivo
|
|
63
|
+
"""
|
|
64
|
+
path = Path(file_path)
|
|
65
|
+
|
|
66
|
+
# Validaciones básicas
|
|
67
|
+
if not path.exists():
|
|
68
|
+
return {"error": f"El archivo {file_path} no existe"}
|
|
69
|
+
|
|
70
|
+
if path.suffix != ".py":
|
|
71
|
+
return {"error": f"El archivo debe ser .py, no {path.suffix}"}
|
|
72
|
+
|
|
73
|
+
# Leer el contenido
|
|
74
|
+
try:
|
|
75
|
+
source_code = path.read_text(encoding="utf-8")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
return {"error": f"No se pudo leer el archivo: {e}"}
|
|
78
|
+
|
|
79
|
+
# Parsear con AST
|
|
80
|
+
try:
|
|
81
|
+
tree = ast.parse(source_code)
|
|
82
|
+
except SyntaxError as e:
|
|
83
|
+
return {"error": f"El archivo tiene errores de sintaxis: {e}"}
|
|
84
|
+
|
|
85
|
+
# Extraer información
|
|
86
|
+
classes = []
|
|
87
|
+
functions = []
|
|
88
|
+
imports = []
|
|
89
|
+
|
|
90
|
+
for node in ast.walk(tree):
|
|
91
|
+
if isinstance(node, ast.ClassDef):
|
|
92
|
+
classes.append({
|
|
93
|
+
"name": node.name,
|
|
94
|
+
"line": node.lineno,
|
|
95
|
+
"methods": [
|
|
96
|
+
n.name for n in node.body
|
|
97
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
98
|
+
]
|
|
99
|
+
})
|
|
100
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
101
|
+
functions.append({
|
|
102
|
+
"name": node.name,
|
|
103
|
+
"line": node.lineno,
|
|
104
|
+
"args": [arg.arg for arg in node.args.args]
|
|
105
|
+
})
|
|
106
|
+
elif isinstance(node, ast.Import):
|
|
107
|
+
for alias in node.names:
|
|
108
|
+
imports.append(alias.name)
|
|
109
|
+
elif isinstance(node, ast.ImportFrom):
|
|
110
|
+
module = node.module or ""
|
|
111
|
+
for alias in node.names:
|
|
112
|
+
imports.append(f"{module}.{alias.name}")
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"file": path.name,
|
|
116
|
+
"total_lines": len(source_code.splitlines()),
|
|
117
|
+
"classes": classes,
|
|
118
|
+
"functions": functions,
|
|
119
|
+
"imports": imports,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
GENERATE_TESTS_PROMPT = """Eres Lanzakit Tester, un ingeniero senior de Python especializado en pytest.
|
|
124
|
+
|
|
125
|
+
Tu tarea: generar un archivo de tests pytest de alta calidad para el código proporcionado. Los tests deben detectar regresiones reales, ser deterministas y enseñar buenas prácticas con comentarios breves en español neutro LATAM.
|
|
126
|
+
|
|
127
|
+
<codigo_a_testear ruta="{file_path}">
|
|
128
|
+
{source_code}
|
|
129
|
+
</codigo_a_testear>
|
|
130
|
+
|
|
131
|
+
<reglas_fundamentales>
|
|
132
|
+
|
|
133
|
+
1. FIDELIDAD AL CÓDIGO
|
|
134
|
+
- Testea el comportamiento OBSERVABLE del código, no lo que crees que debería hacer
|
|
135
|
+
- No inventes contratos ni validaciones que el código no implementa
|
|
136
|
+
- Usa docstrings, type hints y validaciones como fuente de verdad
|
|
137
|
+
|
|
138
|
+
2. EDGE CASES JUSTIFICADOS
|
|
139
|
+
- Cada test debe responder: "¿qué regresión específica detectaría?"
|
|
140
|
+
- Deriva casos de las ramas, operaciones y límites reales del código
|
|
141
|
+
- No incluyas casos genéricos sin justificación
|
|
142
|
+
|
|
143
|
+
3. IMPORTS Y DEPENDENCIAS
|
|
144
|
+
- Solo usa: pytest, unittest.mock, biblioteca estándar
|
|
145
|
+
- NO inventes: fixtures custom, pytest-plugins externos, módulos que no existen
|
|
146
|
+
- Para importar el código bajo prueba usa el nombre del archivo sin extensión
|
|
147
|
+
- Si no puedes determinar la ruta de import con confianza, NO inventes
|
|
148
|
+
|
|
149
|
+
4. PYTEST MODERNO
|
|
150
|
+
- Usa pytest.mark.parametrize cuando varios casos compartan verificación
|
|
151
|
+
- Usa pytest.raises para excepciones
|
|
152
|
+
- Usa fixtures solo cuando reduzcan duplicación real
|
|
153
|
+
- Nombres: test_<funcion>_<escenario>_<resultado>
|
|
154
|
+
|
|
155
|
+
5. COMENTARIOS EDUCATIVOS
|
|
156
|
+
- Comentarios BREVES en español explicando qué regresión protege cada test
|
|
157
|
+
- Un comentario por test o por grupo parametrizado, no por línea
|
|
158
|
+
- Nombres de funciones y variables en inglés (convención Python estándar)
|
|
159
|
+
|
|
160
|
+
6. MOCKING RESPONSABLE
|
|
161
|
+
- No mockees la función bajo prueba
|
|
162
|
+
- Mockea solo fronteras externas (red, archivos, servicios)
|
|
163
|
+
- Prefiere lógica real sin mocks cuando sea posible
|
|
164
|
+
|
|
165
|
+
</reglas_fundamentales>
|
|
166
|
+
|
|
167
|
+
<prohibiciones>
|
|
168
|
+
- NO inventar imports, fixtures, módulos o dependencias
|
|
169
|
+
- NO usar assert True ni aserciones triviales
|
|
170
|
+
- NO capturar Exception genérica para hacer pasar un test
|
|
171
|
+
- NO incluir Markdown, ```python, preámbulos ni explicaciones externas al código
|
|
172
|
+
- NO modificar el código fuente del usuario
|
|
173
|
+
</prohibiciones>
|
|
174
|
+
|
|
175
|
+
<formato_salida>
|
|
176
|
+
Devuelve EXCLUSIVAMENTE código Python válido, listo para guardar como test_<nombre>.py.
|
|
177
|
+
Primera línea: un import o un comentario Python.
|
|
178
|
+
No incluyas nada antes ni después del código.
|
|
179
|
+
</formato_salida>"""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@mcp.tool()
|
|
183
|
+
def generate_tests_for_file(file_path: str) -> dict:
|
|
184
|
+
"""
|
|
185
|
+
Genera tests pytest de alta calidad para un archivo Python usando Claude.
|
|
186
|
+
|
|
187
|
+
Lee el archivo, lo envía a Claude con un prompt optimizado para testing,
|
|
188
|
+
y devuelve el código de los tests generado.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
file_path: Ruta absoluta al archivo .py a testear
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Diccionario con:
|
|
195
|
+
- file: nombre del archivo original
|
|
196
|
+
- test_code: código Python de los tests generados
|
|
197
|
+
- tokens_used: cuántos tokens consumió la llamada
|
|
198
|
+
"""
|
|
199
|
+
path = Path(file_path)
|
|
200
|
+
|
|
201
|
+
if not path.exists():
|
|
202
|
+
return {"error": f"El archivo {file_path} no existe"}
|
|
203
|
+
|
|
204
|
+
if path.suffix != ".py":
|
|
205
|
+
return {"error": f"El archivo debe ser .py, no {path.suffix}"}
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
source_code = path.read_text(encoding="utf-8")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
return {"error": f"No se pudo leer el archivo: {e}"}
|
|
211
|
+
|
|
212
|
+
if not source_code.strip():
|
|
213
|
+
return {"error": "El archivo está vacío"}
|
|
214
|
+
|
|
215
|
+
# Llamar a Claude
|
|
216
|
+
try:
|
|
217
|
+
message = anthropic_client.messages.create(
|
|
218
|
+
model=MODEL,
|
|
219
|
+
max_tokens=16384,
|
|
220
|
+
thinking={"type": "disabled"},
|
|
221
|
+
messages=[
|
|
222
|
+
{
|
|
223
|
+
"role": "user",
|
|
224
|
+
"content": GENERATE_TESTS_PROMPT.format(
|
|
225
|
+
source_code=source_code,
|
|
226
|
+
file_path=str(path)
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
return {"error": f"Error llamando a Claude API: {e}"}
|
|
233
|
+
|
|
234
|
+
# Buscar el primer bloque de texto (puede haber ThinkingBlocks antes)
|
|
235
|
+
test_code = next(
|
|
236
|
+
(block.text for block in message.content if block.type == "text"),
|
|
237
|
+
None
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if test_code is None:
|
|
241
|
+
return {"error": "Claude no devolvió contenido de texto"}
|
|
242
|
+
|
|
243
|
+
# Limpiar markdown fences si los hay
|
|
244
|
+
test_code = _clean_code_output(test_code)
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
"file": path.name,
|
|
248
|
+
"test_code": test_code,
|
|
249
|
+
"tokens_used": {
|
|
250
|
+
"input": message.usage.input_tokens,
|
|
251
|
+
"output": message.usage.output_tokens
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@mcp.tool()
|
|
256
|
+
def generate_and_validate_tests(
|
|
257
|
+
file_path: str,
|
|
258
|
+
save_to: str = ""
|
|
259
|
+
) -> dict:
|
|
260
|
+
"""
|
|
261
|
+
Flujo completo: genera tests, valida sintaxis, ejecuta y reporta.
|
|
262
|
+
|
|
263
|
+
Esta tool cierra el loop de calidad: no solo genera tests sino que
|
|
264
|
+
verifica que compilen y que pasen antes de entregárselos al usuario.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
file_path: Ruta absoluta al archivo .py a testear
|
|
268
|
+
save_to: Ruta donde guardar el archivo de tests (opcional).
|
|
269
|
+
Si está vacío, usa test_<nombre>.py al lado del archivo original.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Dict con:
|
|
273
|
+
- test_code: código de los tests generados
|
|
274
|
+
- saved_to: dónde se guardó
|
|
275
|
+
- syntax_valid: bool
|
|
276
|
+
- execution: resultados de pytest (passed/failed/errors)
|
|
277
|
+
- summary: resumen legible
|
|
278
|
+
"""
|
|
279
|
+
path = Path(file_path)
|
|
280
|
+
|
|
281
|
+
# Paso 1: generar los tests (reutilizamos la tool existente)
|
|
282
|
+
generation_result = generate_tests_for_file(file_path)
|
|
283
|
+
|
|
284
|
+
if "error" in generation_result:
|
|
285
|
+
return generation_result
|
|
286
|
+
|
|
287
|
+
test_code = generation_result["test_code"]
|
|
288
|
+
|
|
289
|
+
# Paso 2: validar sintaxis
|
|
290
|
+
syntax_check = _validate_syntax(test_code)
|
|
291
|
+
|
|
292
|
+
if not syntax_check["valid"]:
|
|
293
|
+
return {
|
|
294
|
+
"test_code": test_code,
|
|
295
|
+
"saved_to": None,
|
|
296
|
+
"syntax_valid": False,
|
|
297
|
+
"syntax_error": syntax_check["error"],
|
|
298
|
+
"execution": None,
|
|
299
|
+
"summary": f"❌ Tests generados pero con error de sintaxis: {syntax_check['error']}",
|
|
300
|
+
"tokens_used": generation_result["tokens_used"],
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Paso 3: determinar dónde guardar
|
|
304
|
+
if save_to:
|
|
305
|
+
save_path = Path(save_to)
|
|
306
|
+
else:
|
|
307
|
+
save_path = path.parent / f"test_{path.stem}.py"
|
|
308
|
+
|
|
309
|
+
# Paso 4: guardar el archivo
|
|
310
|
+
try:
|
|
311
|
+
save_path.write_text(test_code, encoding="utf-8")
|
|
312
|
+
except Exception as e:
|
|
313
|
+
return {
|
|
314
|
+
"test_code": test_code,
|
|
315
|
+
"saved_to": None,
|
|
316
|
+
"syntax_valid": True,
|
|
317
|
+
"execution": None,
|
|
318
|
+
"summary": f"⚠️ Tests válidos pero no se pudieron guardar: {e}",
|
|
319
|
+
"tokens_used": generation_result["tokens_used"],
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Paso 5: ejecutar pytest
|
|
323
|
+
execution = _run_pytest(save_path, source_dir=path.parent)
|
|
324
|
+
|
|
325
|
+
# Paso 6: armar resumen
|
|
326
|
+
if execution.get("all_passed"):
|
|
327
|
+
summary = (
|
|
328
|
+
f"✅ {execution['passed']} tests generados y pasaron. "
|
|
329
|
+
f"Guardado en: {save_path}"
|
|
330
|
+
)
|
|
331
|
+
elif "error" in execution:
|
|
332
|
+
summary = f"⚠️ Tests generados pero no se pudieron ejecutar: {execution['error']}"
|
|
333
|
+
else:
|
|
334
|
+
summary = (
|
|
335
|
+
f"⚠️ {execution.get('passed', 0)} pasaron, "
|
|
336
|
+
f"{execution.get('failed', 0)} fallaron, "
|
|
337
|
+
f"{execution.get('errors', 0)} errores. "
|
|
338
|
+
f"Guardado en: {save_path}"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
"test_code": test_code,
|
|
343
|
+
"saved_to": str(save_path),
|
|
344
|
+
"syntax_valid": True,
|
|
345
|
+
"execution": execution,
|
|
346
|
+
"summary": summary,
|
|
347
|
+
"tokens_used": generation_result["tokens_used"],
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
def _validate_syntax(code: str) -> dict:
|
|
351
|
+
"""Valida que el código sea Python sintácticamente correcto."""
|
|
352
|
+
try:
|
|
353
|
+
ast.parse(code)
|
|
354
|
+
return {"valid": True}
|
|
355
|
+
except SyntaxError as e:
|
|
356
|
+
return {
|
|
357
|
+
"valid": False,
|
|
358
|
+
"error": f"Línea {e.lineno}: {e.msg}",
|
|
359
|
+
"offset": e.offset,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _run_pytest(test_file_path: Path, source_dir: Path, timeout: int = 30) -> dict:
|
|
364
|
+
"""
|
|
365
|
+
Ejecuta pytest sobre un archivo de tests en un subproceso aislado.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
test_file_path: Ruta al archivo de tests
|
|
369
|
+
source_dir: Directorio desde donde ejecutar pytest (para resolución de imports)
|
|
370
|
+
timeout: Segundos máximos antes de matar el proceso
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Dict con: passed, failed, errors, output, stderr
|
|
374
|
+
"""
|
|
375
|
+
try:
|
|
376
|
+
result = subprocess.run(
|
|
377
|
+
[sys.executable, "-m", "pytest", str(test_file_path), "-v", "--tb=short", "--no-header"],
|
|
378
|
+
cwd=str(source_dir),
|
|
379
|
+
capture_output=True,
|
|
380
|
+
text=True,
|
|
381
|
+
timeout=timeout,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
output = result.stdout
|
|
385
|
+
stderr = result.stderr
|
|
386
|
+
|
|
387
|
+
# Parsear resultados básicos del output
|
|
388
|
+
passed = output.count(" PASSED")
|
|
389
|
+
failed = output.count(" FAILED")
|
|
390
|
+
errors = output.count(" ERROR")
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
"passed": passed,
|
|
394
|
+
"failed": failed,
|
|
395
|
+
"errors": errors,
|
|
396
|
+
"all_passed": result.returncode == 0,
|
|
397
|
+
"output": output[-2000:],
|
|
398
|
+
"stderr": stderr[-500:] if stderr else "",
|
|
399
|
+
}
|
|
400
|
+
except subprocess.TimeoutExpired:
|
|
401
|
+
return {
|
|
402
|
+
"error": f"pytest excedió timeout de {timeout}s",
|
|
403
|
+
"all_passed": False,
|
|
404
|
+
}
|
|
405
|
+
except FileNotFoundError:
|
|
406
|
+
return {
|
|
407
|
+
"error": "pytest no está instalado en el entorno",
|
|
408
|
+
"all_passed": False,
|
|
409
|
+
}
|
|
410
|
+
except Exception as e:
|
|
411
|
+
return {
|
|
412
|
+
"error": f"Error ejecutando pytest: {e}",
|
|
413
|
+
"all_passed": False,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
def run() -> None:
|
|
417
|
+
"""Arranca el servidor MCP usando stdio (lo que Claude Desktop espera)."""
|
|
418
|
+
mcp.run(transport="stdio")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
if __name__ == "__main__":
|
|
422
|
+
run()
|