[project] name = "local-delegate-mcp" version = "0.31.0" description = "Delegate mechanical text tasks to a local OpenAI-compatible LLM endpoint to conserve Claude subscription quota." readme = "README.md" requires-python = ">=3.11" license = "MIT" license-files = ["LICENSE"] authors = [{ name = "Yohan González Almaguer" }] keywords = ["mcp", "llm", "local", "openai-compatible", "claude", "llama-swap", "ollama", "quota"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries", "Topic :: Utilities", ] dependencies = [ # Techo de major deliberado, ahora sobre el major 2. Se sube, no se quita: la lección que dejó # la 0.12.1 es que un rango sin techo es una bomba de tiempo (el 2.0.0 eliminó # `mcp.server.fastmcp` y `uvx` resolvió al major nuevo, matando el proceso en el import antes de # hablar MCP), no que este techo concreto sobrara. "mcp>=2,<3", # `httpx2` y no `httpx`: es lo que usa el SDK 2.x, y tener las dos librerías HTTP instaladas a # la vez para siempre es peor que migrar una vez. Starlette 1.3 ya la prefiere para su # `TestClient` y deprecó `httpx`. **Con techo**, y no por simetría: cumple las dos condiciones # del criterio —está en el camino de import de arranque (`server.py` la usa para hablar con el # backend) y su versionado tiene major real—, al revés que `httpx`, que estaba en `0.x` y por # eso no lo llevaba. "httpx2>=2.5,<3", # Techo de major: `platformdirs` y `filelock` están en el **camino de import de arranque** # (`server.py` las carga al importarse, `platformdirs` vía `config.py`), así que si un major # nuevo rompe, el proceso muere antes de poder hablar MCP y el cliente solo ve # `MCP error -32000: Connection closed` — el mismo fallo mudo de la 0.12.1. Las dos usan major # real, así que acotarlo protege de verdad. Criterio completo en `docs/wiki/Repo-hardening.md`. "platformdirs>=4,<5", # `fastapi` y `uvicorn` NO llevan techo, a propósito, pero por razones distintas — conviene no # mezclarlas: # # `fastapi`: está en `0.x` (un `<1` sería decorativo: rompe por minor) **y** no está en el # camino de arranque, porque `web/metrics.py` entra por import perezoso. `tests/test_core.py` # fija esa invariante para que un refactor no la rompa en silencio. # # `uvicorn`: está en `0.x` igual, pero **sí se carga al arrancar** — y no por culpa de este # paquete: lo arrastra el SDK. Quien gobierna de verdad esa compatibilidad es el rango de # `sse-starlette`, que es transitiva y no se declara aquí; un techo nuestro daría cobertura # aparente sin cambiar nada. Verificado por ejecución, no supuesto. "fastapi>=0.115", "uvicorn>=0.30", "filelock>=3,<4", ] [project.optional-dependencies] # Extra opt-in para los CLIs `check-llamaswap`/`init-llamaswap` (groups de llama-swap). # Sin este extra el paquete se comporta exactamente igual que hoy: nada lo requiere en # el flujo normal del MCP. llamaswap = ["pyyaml>=6"] [project.scripts] # Dos alias al mismo entry point: `local-delegate-mcp` hace que `uvx local-delegate-mcp` # funcione directo (comando == nombre del paquete); `local-delegate` es el alias corto. local-delegate-mcp = "local_delegate:main" local-delegate = "local_delegate:main" [project.urls] Homepage = "https://github.com/ZahiriNatZuke/local-delegate" Repository = "https://github.com/ZahiriNatZuke/local-delegate" Issues = "https://github.com/ZahiriNatZuke/local-delegate/issues" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/local_delegate"] [tool.hatch.build.targets.sdist] exclude = [ "/.codex", "/.sdd", "/.venv", "/dist", # `scripts/` es el taller del repositorio (release, bump de versión, canarios, handshake de # instalación): nada de ahí lo ejecuta quien instala el paquete, y publicarlo solo amplía la # superficie que un auditor de cadena de suministro tiene que mirar. Quien desarrolla clona el # repositorio; el sdist distribuye el paquete, no el taller. "/scripts", ] [dependency-groups] dev = [ "pytest>=8", "ruff>=0.6", "pre-commit>=3.7", # `respx` sale de la suite: declara `httpx>=0.25.0` y no soporta `httpx2`, así que dejarlo # arrastraría de vuelta la librería que la migración quita. Su sitio lo ocupa # `httpx2.MockTransport`. "pyyaml>=6", ] # Playwright, en su propio grupo y **fuera de `dev`**. Dos motivos, los dos medidos: # # 1. Estaba sin declarar en ninguna parte, aunque `scripts/dev/capture_dashboard.py` y el flujo de # la captura del README lo necesitan. Por eso `uv sync` lo desinstalaba y las capturas dejaban # de funcionar sin que nada avisara — una trampa sufrida más de una vez y nunca arreglada aquí. # 2. Fuera de `dev` porque arrastra un navegador de ~150 MB que hay que instalar aparte # (`playwright install chromium`). Meterlo en `dev` obligaría a todo el que toca el repo a # pagarlo; así lo pide quien lo necesita, y el CI lo instala solo en el job que lo usa. # # `tests/test_dashboard_ui.py` se salta solo si falta, y lleva su propia guarda para que ese salto # no pueda pasar desapercibido en el CI. ui = ["playwright>=1.40"] [tool.ruff] target-version = "py311" line-length = 100 [tool.ruff.lint] # Reglas por defecto (E4/E7/E9, F) + imports ordenados (I). extend-select = ["I"] # ruff 0.16 promovió a estables reglas que chocan con decisiones deliberadas de este proyecto. # Se ignoran con su motivo, en vez de reescribir código que está bien a propósito. ignore = [ # El diagnóstico (`doctor`), la lectura de RAM/VRAM/procesos (`web/sysinfo`) y el # instalador son **best-effort**: un binario ausente, una salida inesperada o un fallo de # red degradan a un aviso y NUNCA lanzan — está escrito en sus docstrings. Ahí # `except Exception` es el contrato, no un descuido, y un `pass` silencioso es la # degradación correcta: reventar el dashboard porque `nvidia-smi` no está sería peor. "BLE001", "S110", # `subprocess.run` sin `check=` es deliberado: el returncode se comprueba a mano para dar # un mensaje propio y accionable en vez de un CalledProcessError crudo. "PLW1510", ] [tool.ruff.format] # ruff 0.16 formatea también los bloques de código Python embebidos en Markdown. La doc de # `docs/plans/` es registro histórico de decisiones y los ejemplos de la wiki se escriben para # leerse (comentarios alineados a mano); reformatearlos automáticamente solo genera ruido. exclude = ["*.md"] [tool.pytest.ini_options] testpaths = ["tests"]