zerochat 8.1.0__tar.gz → 8.3.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.
- {zerochat-8.1.0/zerochat.egg-info → zerochat-8.3.0}/PKG-INFO +1 -1
- {zerochat-8.1.0 → zerochat-8.3.0}/pyproject.toml +1 -1
- {zerochat-8.1.0 → zerochat-8.3.0/zerochat.egg-info}/PKG-INFO +1 -1
- {zerochat-8.1.0 → zerochat-8.3.0}/zerochat.py +175 -89
- {zerochat-8.1.0 → zerochat-8.3.0}/LICENSE +0 -0
- {zerochat-8.1.0 → zerochat-8.3.0}/README.md +0 -0
- {zerochat-8.1.0 → zerochat-8.3.0}/setup.cfg +0 -0
- {zerochat-8.1.0 → zerochat-8.3.0}/zerochat.egg-info/SOURCES.txt +0 -0
- {zerochat-8.1.0 → zerochat-8.3.0}/zerochat.egg-info/dependency_links.txt +0 -0
- {zerochat-8.1.0 → zerochat-8.3.0}/zerochat.egg-info/entry_points.txt +0 -0
- {zerochat-8.1.0 → zerochat-8.3.0}/zerochat.egg-info/top_level.txt +0 -0
|
@@ -42,7 +42,7 @@ import webbrowser
|
|
|
42
42
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
43
43
|
from pathlib import Path
|
|
44
44
|
|
|
45
|
-
SOURCE_BACKEND_VERSION = "8.
|
|
45
|
+
SOURCE_BACKEND_VERSION = "8.3.0"
|
|
46
46
|
|
|
47
47
|
def _read_source_version(filename: str) -> str | None:
|
|
48
48
|
"""Lee la versión de un archivo del repositorio cuando se ejecuta desde fuentes."""
|
|
@@ -93,9 +93,11 @@ UI_VERSION = _read_ui_version()
|
|
|
93
93
|
DEFAULT_PORT = 6388
|
|
94
94
|
DEFAULT_HOST = "127.0.0.1"
|
|
95
95
|
DEFAULT_UI_URL = "https://albalday.github.io/zerochat/zerochat.html"
|
|
96
|
-
REMOTE_VERSION_URL = "https://raw.githubusercontent.com/albalday/zerochat/master/package.json"
|
|
97
|
-
PYPI_VERSION_URL = "https://pypi.org/pypi/zerochat/json"
|
|
98
96
|
REMOTE_SCRIPT_URL = "https://raw.githubusercontent.com/albalday/zerochat/master/zerochat.py"
|
|
97
|
+
# Las versiones remotas se consultan en las dos distribuciones que consume el
|
|
98
|
+
# usuario: la interfaz publicada en GitHub Pages y el ejecutable en GitHub.
|
|
99
|
+
REMOTE_UI_VERSION_URL = DEFAULT_UI_URL
|
|
100
|
+
REMOTE_BACKEND_VERSION_URL = REMOTE_SCRIPT_URL
|
|
99
101
|
CONSOLE_STATUS_IDLE_SECONDS = 8.0
|
|
100
102
|
CONSOLE_CONTROL = None
|
|
101
103
|
NOTICES: list[str] = []
|
|
@@ -335,22 +337,24 @@ def parse_version(ver: str) -> tuple[int, ...]:
|
|
|
335
337
|
return tuple(parts)
|
|
336
338
|
|
|
337
339
|
|
|
338
|
-
def
|
|
339
|
-
"""
|
|
340
|
+
def _read_remote_content(url: str) -> str:
|
|
341
|
+
"""Lee una cabecera acotada de un recurso publicado de ZeroChat."""
|
|
340
342
|
req = urllib.request.Request(url, headers={"User-Agent": f"ZeroChat/{VERSION}"})
|
|
341
343
|
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
344
|
+
return resp.read(4096).decode("utf-8", errors="ignore")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _read_remote_ui_version() -> str | None:
|
|
348
|
+
"""Extrae la versión del título de la interfaz servida por GitHub Pages."""
|
|
349
|
+
content = _read_remote_content(REMOTE_UI_VERSION_URL)
|
|
350
|
+
match = re.search(r"<title>\s*ZeroChat\s+v(\d+\.\d+\.\d+)\s*</title>", content, re.IGNORECASE)
|
|
351
|
+
return match.group(1) if match else None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _read_remote_backend_version() -> str | None:
|
|
355
|
+
"""Extrae la versión estática del ejecutable publicado en GitHub."""
|
|
356
|
+
content = _read_remote_content(REMOTE_BACKEND_VERSION_URL)
|
|
357
|
+
match = re.search(r'^SOURCE_BACKEND_VERSION\s*=\s*["\'](\d+\.\d+\.\d+)["\']', content, re.MULTILINE)
|
|
354
358
|
return match.group(1) if match else None
|
|
355
359
|
|
|
356
360
|
|
|
@@ -360,14 +364,21 @@ def has_new_backend_version(remote_version: str, local_version: str = VERSION) -
|
|
|
360
364
|
|
|
361
365
|
|
|
362
366
|
def check_version():
|
|
363
|
-
"""Informa de
|
|
367
|
+
"""Informa de la versión de Pages y de actualizaciones del ejecutable."""
|
|
364
368
|
if get_dev_root() is not None:
|
|
365
369
|
return
|
|
366
370
|
try:
|
|
367
371
|
installed = is_installed_runtime()
|
|
368
|
-
|
|
369
|
-
if
|
|
370
|
-
notice = f"
|
|
372
|
+
remote_ui_version = _read_remote_ui_version()
|
|
373
|
+
if remote_ui_version:
|
|
374
|
+
notice = f"Interfaz web en GitHub Pages: v{remote_ui_version}."
|
|
375
|
+
if remote_ui_version != UI_VERSION:
|
|
376
|
+
notice += f" El ejecutable incluye la referencia v{UI_VERSION}; la interfaz remota se cargará al abrir el navegador."
|
|
377
|
+
add_notice(notice)
|
|
378
|
+
|
|
379
|
+
remote_backend_version = _read_remote_backend_version()
|
|
380
|
+
if remote_backend_version and has_new_backend_version(remote_backend_version):
|
|
381
|
+
notice = f"Nueva versión del servidor disponible (local: {VERSION}, remota: {compatibility_version(remote_backend_version)})."
|
|
371
382
|
if installed:
|
|
372
383
|
notice += f" Actualiza cuando quieras con: {sys.executable} -m pip install --upgrade --no-cache-dir zerochat"
|
|
373
384
|
else:
|
|
@@ -1488,8 +1499,16 @@ def public_tool_name(server_id: str, original: str) -> str:
|
|
|
1488
1499
|
return name
|
|
1489
1500
|
|
|
1490
1501
|
|
|
1502
|
+
def _mcp_trace_text(value: object, max_len: int = 1000) -> str:
|
|
1503
|
+
"""Normaliza diagnósticos MCP para consola sin exponer secretos comunes."""
|
|
1504
|
+
text = " ".join(str(value).strip().splitlines())
|
|
1505
|
+
text = re.sub(r"(?i)(\b(?:api[_-]?key|token|secret|password)\b\s*[=:]\s*)\S+", r"\1***", text)
|
|
1506
|
+
text = re.sub(r"(?i)(--(?:api[_-]?key|token|secret|password)\s+)\S+", r"\1***", text)
|
|
1507
|
+
return text[:max_len - 3] + "..." if len(text) > max_len else text
|
|
1508
|
+
|
|
1509
|
+
|
|
1491
1510
|
class StdioMcpClient:
|
|
1492
|
-
def __init__(self, command: str, args: list[str], cwd: str, env: dict[str, str]):
|
|
1511
|
+
def __init__(self, command: str, args: list[str], cwd: str, env: dict[str, str], trace=None):
|
|
1493
1512
|
self.command = command
|
|
1494
1513
|
self.args = args
|
|
1495
1514
|
self.cwd = cwd
|
|
@@ -1500,12 +1519,14 @@ class StdioMcpClient:
|
|
|
1500
1519
|
self._lock = threading.Lock()
|
|
1501
1520
|
self._alive = False
|
|
1502
1521
|
self.tools: list[dict] = []
|
|
1522
|
+
self._trace = trace or (lambda _message: None)
|
|
1503
1523
|
|
|
1504
1524
|
def running(self) -> bool:
|
|
1505
1525
|
return self._alive and self.process is not None and self.process.poll() is None
|
|
1506
1526
|
|
|
1507
1527
|
def start(self, handshake_timeout: int = 30):
|
|
1508
1528
|
cmd = [self.command] + self.args
|
|
1529
|
+
self._trace(f"launching: {_mcp_trace_text(' '.join(cmd))}")
|
|
1509
1530
|
self.process = subprocess.Popen(
|
|
1510
1531
|
cmd,
|
|
1511
1532
|
stdin=subprocess.PIPE,
|
|
@@ -1530,11 +1551,14 @@ class StdioMcpClient:
|
|
|
1530
1551
|
self.notify("notifications/initialized")
|
|
1531
1552
|
tools_resp = self.request("tools/list", {}, timeout=10)
|
|
1532
1553
|
self.tools = tools_resp.get("tools", [])
|
|
1554
|
+
self._trace(f"handshake completed; {len(self.tools)} tool(s) available")
|
|
1533
1555
|
|
|
1534
1556
|
def _drain_stderr(self):
|
|
1535
1557
|
if self.process and self.process.stderr:
|
|
1536
|
-
for
|
|
1537
|
-
|
|
1558
|
+
for line in self.process.stderr:
|
|
1559
|
+
line = _mcp_trace_text(line)
|
|
1560
|
+
if line:
|
|
1561
|
+
self._trace(f"stderr: {line}")
|
|
1538
1562
|
|
|
1539
1563
|
def _read_stdout(self):
|
|
1540
1564
|
try:
|
|
@@ -1556,6 +1580,10 @@ class StdioMcpClient:
|
|
|
1556
1580
|
waiter.put(msg)
|
|
1557
1581
|
finally:
|
|
1558
1582
|
self._alive = False
|
|
1583
|
+
if self.process:
|
|
1584
|
+
return_code = self.process.poll()
|
|
1585
|
+
if return_code is not None:
|
|
1586
|
+
self._trace(f"process exited with code {return_code}")
|
|
1559
1587
|
with self._lock:
|
|
1560
1588
|
pending = list(self._pending.values())
|
|
1561
1589
|
self._pending.clear()
|
|
@@ -1618,11 +1646,7 @@ class McpServiceManager:
|
|
|
1618
1646
|
if services_root:
|
|
1619
1647
|
self.services_root = Path(services_root)
|
|
1620
1648
|
else:
|
|
1621
|
-
|
|
1622
|
-
Path.cwd() / "services",
|
|
1623
|
-
get_data_dir() / "services"
|
|
1624
|
-
]
|
|
1625
|
-
self.services_root = candidates[0] if get_dev_root() is not None and candidates[0].is_dir() else candidates[1]
|
|
1649
|
+
self.services_root = get_data_dir() / "services"
|
|
1626
1650
|
self.services_root.mkdir(parents=True, exist_ok=True)
|
|
1627
1651
|
self.config_file = get_data_dir() / "config" / "services.json"
|
|
1628
1652
|
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -1634,37 +1658,50 @@ class McpServiceManager:
|
|
|
1634
1658
|
self.services = self._load_services()
|
|
1635
1659
|
self.preferences = self._load_preferences()
|
|
1636
1660
|
|
|
1661
|
+
@staticmethod
|
|
1662
|
+
def _trace(server_id: str, message: str):
|
|
1663
|
+
console_log(f"[{time.strftime('%H:%M:%S')}] [MCP {server_id}] {_mcp_trace_text(message)}", flush=True)
|
|
1664
|
+
|
|
1637
1665
|
def _ensure_default_services(self):
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
service_json_file =
|
|
1641
|
-
|
|
1666
|
+
example_dir = self.services_root / "ejemplo"
|
|
1667
|
+
example_dir.mkdir(parents=True, exist_ok=True)
|
|
1668
|
+
service_json_file = example_dir / "service.json"
|
|
1669
|
+
example_server_file = example_dir / "ejemplo_mcp_server.py"
|
|
1670
|
+
example_readme_file = example_dir / "README.md"
|
|
1671
|
+
installer_example_file = example_dir / "installer.json.example"
|
|
1642
1672
|
|
|
1643
1673
|
if not service_json_file.exists():
|
|
1644
1674
|
service_json_file.write_text(json.dumps({
|
|
1645
1675
|
"schemaVersion": 1,
|
|
1646
|
-
"id": "
|
|
1676
|
+
"id": "ejemplo",
|
|
1647
1677
|
"displayName": {
|
|
1648
|
-
"es": "
|
|
1649
|
-
"en": "
|
|
1678
|
+
"es": "Ejemplo de MCP",
|
|
1679
|
+
"en": "MCP example"
|
|
1650
1680
|
},
|
|
1651
1681
|
"description": {
|
|
1652
|
-
"es": "
|
|
1653
|
-
"en": "Minimal MCP
|
|
1682
|
+
"es": "Plantilla mínima para crear servicios MCP gestionados por ZeroChat. Muestra el protocolo stdio y la estructura de configuración que debe seguir un nuevo MCP.",
|
|
1683
|
+
"en": "Minimal template for creating ZeroChat-managed MCP services. It demonstrates the stdio protocol and configuration structure for a new MCP."
|
|
1684
|
+
},
|
|
1685
|
+
"help": {
|
|
1686
|
+
"url": "help/mcp.html#crear-mcp-con-agente",
|
|
1687
|
+
"label": {
|
|
1688
|
+
"es": "Guía para crear un MCP gestionado",
|
|
1689
|
+
"en": "Guide to creating a managed MCP"
|
|
1690
|
+
}
|
|
1654
1691
|
},
|
|
1655
1692
|
"enabledByDefault": False,
|
|
1656
1693
|
"transport": "stdio",
|
|
1657
1694
|
"launch": {
|
|
1658
1695
|
"executable": "${pythonExecutable}",
|
|
1659
|
-
"args": ["${serviceDir}/
|
|
1696
|
+
"args": ["${serviceDir}/ejemplo_mcp_server.py"],
|
|
1660
1697
|
"cwd": "${serviceDir}",
|
|
1661
1698
|
"env": {},
|
|
1662
1699
|
"handshakeTimeoutSeconds": 10
|
|
1663
1700
|
}
|
|
1664
1701
|
}, indent=2), encoding="utf-8")
|
|
1665
1702
|
|
|
1666
|
-
if not
|
|
1667
|
-
|
|
1703
|
+
if not example_server_file.exists():
|
|
1704
|
+
example_server_file.write_text('''#!/usr/bin/env python3
|
|
1668
1705
|
import json, sys
|
|
1669
1706
|
|
|
1670
1707
|
def reply(req_id, result=None, error=None):
|
|
@@ -1681,7 +1718,7 @@ for raw in sys.stdin:
|
|
|
1681
1718
|
if method == "initialize":
|
|
1682
1719
|
reply(req_id, {
|
|
1683
1720
|
"protocolVersion": "2024-11-05",
|
|
1684
|
-
"serverInfo": {"name": "ZeroChat
|
|
1721
|
+
"serverInfo": {"name": "ZeroChat MCP Example", "version": "1.0.0"},
|
|
1685
1722
|
"capabilities": {"tools": {}}
|
|
1686
1723
|
})
|
|
1687
1724
|
elif method == "tools/list":
|
|
@@ -1705,6 +1742,66 @@ for raw in sys.stdin:
|
|
|
1705
1742
|
})
|
|
1706
1743
|
''', encoding="utf-8")
|
|
1707
1744
|
|
|
1745
|
+
if not installer_example_file.exists():
|
|
1746
|
+
installer_example_file.write_text(json.dumps({
|
|
1747
|
+
"schemaVersion": 1,
|
|
1748
|
+
"type": "npm",
|
|
1749
|
+
"product": {
|
|
1750
|
+
"package": "@scope/mcp-package",
|
|
1751
|
+
"version": "1.2.3"
|
|
1752
|
+
},
|
|
1753
|
+
"requirements": {
|
|
1754
|
+
"node": {"minimumMajor": 18}
|
|
1755
|
+
}
|
|
1756
|
+
}, indent=2) + "\n", encoding="utf-8")
|
|
1757
|
+
|
|
1758
|
+
if not example_readme_file.exists():
|
|
1759
|
+
example_readme_file.write_text('''# Ejemplo de MCP gestionado\n\nEsta carpeta es la plantilla para que un agente cree un servicio MCP gestionado por ZeroChat. Para crear uno nuevo, copie esta estructura en `~/zerochat/services/<id-seguro>/`; no modifique este ejemplo.\n\n## Archivos\n\n- `service.json` es obligatorio. Define el identificador, los textos visibles, el proceso y el campo estándar opcional `help`.\n- `installer.json` es opcional. Créelo a partir de `installer.json.example` solo si ZeroChat debe instalar un paquete npm. Fije siempre una versión exacta.\n- El ejecutable o script de arranque se declara en `launch`. Use `${serviceDir}`, `${pythonExecutable}` o `${nodeExecutable}` en lugar de rutas de usuario.\n\n## Campo de ayuda\n\nTodos los servicios pueden incluir:\n\n```json\n"help": {\n "url": "help/mcp.html",\n "label": { "es": "Texto en español", "en": "English text" }\n}\n```\n\nLa interfaz muestra ese enlace en la tarjeta del servicio. `url` debe ser una ruta relativa de ZeroChat o una URL `https:` o `http:`.\n\n## Instrucción para un agente\n\nCopia este texto y sustituye solo `<nombre-del-mcp>`:\n\n```text\nSiguiendo estrictamente la estructura de ~/zerochat/services/ejemplo, busca en Internet la documentación oficial de instalación del MCP <nombre-del-mcp> y crea en ~/zerochat/services/ el directorio y los JSON necesarios para instalarlo y ejecutarlo en ZeroChat. Usa exclusivamente fuentes oficiales, fija versiones concretas, añade la sección help con el enlace a la documentación oficial, no incluyas secretos y no inicies el servicio. Al terminar, resume los archivos creados, las fuentes consultadas y los requisitos pendientes.\n```\n''', encoding="utf-8")
|
|
1760
|
+
|
|
1761
|
+
example_readme_file.write_text('''# Crear un MCP gestionado por ZeroChat
|
|
1762
|
+
|
|
1763
|
+
Esta carpeta es la referencia para crear servicios MCP gestionados. La ubicación de producción es el directorio personal del usuario, seguido de `zerochat/services` (por ejemplo, `/home/usuario/zerochat/services`). No uses `~`, `$HOME` ni una ruta relativa como `services/`: primero identifica la ruta absoluta del directorio personal y úsala para localizar este archivo. El nuevo servicio se crea como carpeta hermana de `ejemplo`, nunca dentro de ella.
|
|
1764
|
+
|
|
1765
|
+
## Procedimiento obligatorio
|
|
1766
|
+
|
|
1767
|
+
1. Lee `README.md`, `service.json` y `installer.json.example` de la carpeta `ejemplo` situada en el directorio de datos de ZeroChat.
|
|
1768
|
+
2. Consulta solo la documentación oficial del MCP solicitado.
|
|
1769
|
+
3. Crea una carpeta con un identificador seguro y descriptivo junto a `ejemplo`.
|
|
1770
|
+
4. Crea `service.json` copiando esta estructura y adapta únicamente los valores necesarios.
|
|
1771
|
+
5. Crea `installer.json` solo si el MCP se distribuye como paquete npm. Copia `installer.json.example`, fija la versión exacta y declara la versión mínima de Node.js.
|
|
1772
|
+
6. Añade `help.url` con la página oficial de instalación y etiquetas en español e inglés.
|
|
1773
|
+
7. No incluyas tokens, contraseñas ni valores privados. Decláralos en `launch.env` con un marcador, por ejemplo `${HOME_ASSISTANT_TOKEN}`.
|
|
1774
|
+
8. No inicies el servicio. Al terminar, informa de los archivos creados, fuentes oficiales y requisitos pendientes.
|
|
1775
|
+
|
|
1776
|
+
## Límites de ZeroChat
|
|
1777
|
+
|
|
1778
|
+
ZeroChat gestiona procesos MCP por `stdio`. Puede instalar paquetes npm mediante `installer.json` y arrancarlos con `${nodeExecutable}`. También puede arrancar un ejecutable o script ya disponible con `launch`.
|
|
1779
|
+
|
|
1780
|
+
No inventes una configuración para un MCP que requiera OAuth interactivo, transporte remoto HTTP/SSE, Docker, Python u otro instalador no soportado por estos JSON. En ese caso, no crees archivos: explica el requisito y qué soporte faltaría.
|
|
1781
|
+
|
|
1782
|
+
## Contratos JSON
|
|
1783
|
+
|
|
1784
|
+
- `service.json` es obligatorio. Conserva `schemaVersion`, `id`, textos bilingües, `transport: "stdio"` y `launch`.
|
|
1785
|
+
- En `launch`, usa `${serviceDir}`, `${nodeExecutable}` o `${pythonExecutable}`; no uses rutas absolutas de usuario.
|
|
1786
|
+
- `installer.json` es opcional y hoy solo admite `type: "npm"`.
|
|
1787
|
+
- `help` es opcional, pero debe incluirse en todo servicio creado:
|
|
1788
|
+
|
|
1789
|
+
```json
|
|
1790
|
+
"help": {
|
|
1791
|
+
"url": "https://documentacion-oficial.example/install",
|
|
1792
|
+
"label": { "es": "Instalación oficial", "en": "Official installation" }
|
|
1793
|
+
}
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
## Prompt para el usuario
|
|
1797
|
+
|
|
1798
|
+
Sustituye únicamente `<nombre-del-mcp>`:
|
|
1799
|
+
|
|
1800
|
+
```text
|
|
1801
|
+
Para instalar el MCP <nombre-del-mcp>, localiza el directorio personal del usuario y lee primero el archivo zerochat/services/ejemplo/README.md usando su ruta absoluta. Sigue exactamente sus instrucciones.
|
|
1802
|
+
```
|
|
1803
|
+
''', encoding="utf-8")
|
|
1804
|
+
|
|
1708
1805
|
# 2. playwright
|
|
1709
1806
|
playwright_dir = self.services_root / "playwright"
|
|
1710
1807
|
playwright_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1756,8 +1853,10 @@ for raw in sys.stdin:
|
|
|
1756
1853
|
"product": {
|
|
1757
1854
|
"package": "@playwright/mcp",
|
|
1758
1855
|
"version": "0.0.81",
|
|
1759
|
-
"browser": "chromium"
|
|
1760
|
-
|
|
1856
|
+
"browser": "chromium"
|
|
1857
|
+
},
|
|
1858
|
+
"requirements": {
|
|
1859
|
+
"node": {"minimumMajor": 18}
|
|
1761
1860
|
}
|
|
1762
1861
|
}, indent=2), encoding="utf-8")
|
|
1763
1862
|
|
|
@@ -1796,46 +1895,10 @@ for raw in sys.stdin:
|
|
|
1796
1895
|
"type": "npm",
|
|
1797
1896
|
"product": {
|
|
1798
1897
|
"package": "@modelcontextprotocol/server-memory",
|
|
1799
|
-
"version": "2026.8.31"
|
|
1800
|
-
"nodeMajor": 18
|
|
1801
|
-
}
|
|
1802
|
-
}, indent=2), encoding="utf-8")
|
|
1803
|
-
|
|
1804
|
-
# 4. lsp
|
|
1805
|
-
lsp_dir = self.services_root / "lsp"
|
|
1806
|
-
lsp_dir.mkdir(parents=True, exist_ok=True)
|
|
1807
|
-
lsp_service = lsp_dir / "service.json"
|
|
1808
|
-
lsp_installer = lsp_dir / "installer.json"
|
|
1809
|
-
if not lsp_service.exists():
|
|
1810
|
-
lsp_service.write_text(json.dumps({
|
|
1811
|
-
"schemaVersion": 1,
|
|
1812
|
-
"id": "lsp",
|
|
1813
|
-
"displayName": {
|
|
1814
|
-
"es": "LSP y Navegación de Código",
|
|
1815
|
-
"en": "LSP & Code Intelligence"
|
|
1816
|
-
},
|
|
1817
|
-
"description": {
|
|
1818
|
-
"es": "Servidor de protocolos de lenguaje (LSP): salto a definiciones, búsqueda de símbolos, referencias e inspección de tipos sin sobrecargar el contexto.",
|
|
1819
|
-
"en": "Language Server Protocol (LSP) server: jump to definitions, symbol search, references, and type inspection without context overload."
|
|
1898
|
+
"version": "2026.8.31"
|
|
1820
1899
|
},
|
|
1821
|
-
"
|
|
1822
|
-
|
|
1823
|
-
"launch": {
|
|
1824
|
-
"executable": "${nodeExecutable}",
|
|
1825
|
-
"args": ["${serviceDir}/node_modules/@axivo/mcp-lsp/dist/index.js"],
|
|
1826
|
-
"cwd": "${serviceDir}",
|
|
1827
|
-
"env": {},
|
|
1828
|
-
"handshakeTimeoutSeconds": 30
|
|
1829
|
-
}
|
|
1830
|
-
}, indent=2), encoding="utf-8")
|
|
1831
|
-
if not lsp_installer.exists():
|
|
1832
|
-
lsp_installer.write_text(json.dumps({
|
|
1833
|
-
"schemaVersion": 1,
|
|
1834
|
-
"type": "npm",
|
|
1835
|
-
"product": {
|
|
1836
|
-
"package": "@axivo/mcp-lsp",
|
|
1837
|
-
"version": "1.0.5",
|
|
1838
|
-
"nodeMajor": 24
|
|
1900
|
+
"requirements": {
|
|
1901
|
+
"node": {"minimumMajor": 18}
|
|
1839
1902
|
}
|
|
1840
1903
|
}, indent=2), encoding="utf-8")
|
|
1841
1904
|
|
|
@@ -1884,6 +1947,7 @@ for raw in sys.stdin:
|
|
|
1884
1947
|
"id": server_id,
|
|
1885
1948
|
"displayName": server.get("displayName", {}),
|
|
1886
1949
|
"description": server.get("description", {}),
|
|
1950
|
+
"help": server.get("help"),
|
|
1887
1951
|
"enabled": pref.get("enabled", server.get("enabledByDefault", False)),
|
|
1888
1952
|
"status": "running" if running else self.states.get(server_id, "stopped"),
|
|
1889
1953
|
"toolCount": len(client.tools) if running else 0,
|
|
@@ -1907,6 +1971,7 @@ for raw in sys.stdin:
|
|
|
1907
1971
|
npm = shutil.which("npm") or "npm"
|
|
1908
1972
|
|
|
1909
1973
|
if installer_file.exists():
|
|
1974
|
+
self._trace(server_id, f"reading installer: {installer_file.name}")
|
|
1910
1975
|
marker = service_dir / ".installed.json"
|
|
1911
1976
|
try:
|
|
1912
1977
|
installer = json.loads(installer_file.read_text(encoding="utf-8"))
|
|
@@ -1922,16 +1987,24 @@ for raw in sys.stdin:
|
|
|
1922
1987
|
if not shutil.which("node") or not shutil.which("npm"):
|
|
1923
1988
|
raise RuntimeError("Node.js y npm son necesarios para instalar este servicio MCP")
|
|
1924
1989
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1990
|
+
requirements = installer.get("requirements", {})
|
|
1991
|
+
node_requirement = requirements.get("node")
|
|
1992
|
+
if node_requirement is None:
|
|
1993
|
+
# Compatibilidad con instaladores creados antes de requirements.node.
|
|
1994
|
+
required_node_major = int(product.get("nodeMajor", 18))
|
|
1995
|
+
elif not isinstance(node_requirement, dict):
|
|
1996
|
+
raise RuntimeError("El requisito Node.js debe ser un objeto en installer.json")
|
|
1997
|
+
else:
|
|
1998
|
+
required_node_major = node_requirement.get("minimumMajor")
|
|
1999
|
+
if not isinstance(required_node_major, int) or isinstance(required_node_major, bool) or required_node_major < 1:
|
|
2000
|
+
raise RuntimeError("El requisito Node.js debe definir minimumMajor como un entero positivo")
|
|
1929
2001
|
version_result = subprocess.run(
|
|
1930
2002
|
[node, "--version"], capture_output=True, text=True, timeout=5
|
|
1931
2003
|
)
|
|
1932
|
-
version_match = re.match(r"v(
|
|
2004
|
+
version_match = re.match(r"v(\d+)", version_result.stdout.strip())
|
|
1933
2005
|
if version_result.returncode != 0 or not version_match:
|
|
1934
2006
|
raise RuntimeError("No se pudo comprobar la versión de Node.js necesaria para este servicio MCP")
|
|
2007
|
+
self._trace(server_id, f"Node.js detected: {version_result.stdout.strip()} (minimum {required_node_major})")
|
|
1935
2008
|
if int(version_match.group(1)) < required_node_major:
|
|
1936
2009
|
raise RuntimeError(
|
|
1937
2010
|
f"{server_id} requiere Node.js {required_node_major}+; se detectó {version_result.stdout.strip()}"
|
|
@@ -1949,11 +2022,13 @@ for raw in sys.stdin:
|
|
|
1949
2022
|
|
|
1950
2023
|
if needs_install:
|
|
1951
2024
|
self.states[server_id] = "installing"
|
|
2025
|
+
self._trace(server_id, f"installing npm package: {package}@{version}")
|
|
1952
2026
|
manifest = service_dir / "package.json"
|
|
1953
2027
|
manifest.write_text(json.dumps({"private": True, "dependencies": {package: version}}, indent=2), encoding="utf-8")
|
|
1954
2028
|
res = subprocess.run([npm, "install", "--ignore-scripts", "--no-audit", "--no-fund"], cwd=service_dir, capture_output=True, text=True, timeout=600)
|
|
1955
2029
|
if res.returncode != 0:
|
|
1956
2030
|
raise RuntimeError(f"Fallo instalando dependencias npm: {res.stderr or res.stdout}")
|
|
2031
|
+
self._trace(server_id, f"npm package installed: {package}@{version}")
|
|
1957
2032
|
installation = {"type": "npm", "package": package, "version": version, "nodeExecutable": node}
|
|
1958
2033
|
browser = product.get("browser")
|
|
1959
2034
|
if browser:
|
|
@@ -1971,6 +2046,8 @@ for raw in sys.stdin:
|
|
|
1971
2046
|
)
|
|
1972
2047
|
installation["browser"] = browser
|
|
1973
2048
|
marker.write_text(json.dumps(installation, indent=2), encoding="utf-8")
|
|
2049
|
+
else:
|
|
2050
|
+
self._trace(server_id, f"npm package already installed: {package}@{version}")
|
|
1974
2051
|
|
|
1975
2052
|
return {
|
|
1976
2053
|
"serviceDir": str(service_dir),
|
|
@@ -1988,6 +2065,8 @@ for raw in sys.stdin:
|
|
|
1988
2065
|
if current and current.running():
|
|
1989
2066
|
return self.list_servers()
|
|
1990
2067
|
self.states[server_id] = "starting"
|
|
2068
|
+
client = None
|
|
2069
|
+
self._trace(server_id, "start requested")
|
|
1991
2070
|
try:
|
|
1992
2071
|
service_dir = server["_directory"]
|
|
1993
2072
|
values = self._prepare_service(server_id, server)
|
|
@@ -2015,7 +2094,10 @@ for raw in sys.stdin:
|
|
|
2015
2094
|
for k, v in launch.get("env", {}).items():
|
|
2016
2095
|
env[k] = self._expand(v, values)
|
|
2017
2096
|
|
|
2018
|
-
client = StdioMcpClient(
|
|
2097
|
+
client = StdioMcpClient(
|
|
2098
|
+
command, args, str(service_dir), env,
|
|
2099
|
+
trace=lambda message: self._trace(server_id, message)
|
|
2100
|
+
)
|
|
2019
2101
|
client.start(int(launch.get("handshakeTimeoutSeconds", 15)))
|
|
2020
2102
|
self.clients[server_id] = client
|
|
2021
2103
|
self.states[server_id] = "running"
|
|
@@ -2023,11 +2105,15 @@ for raw in sys.stdin:
|
|
|
2023
2105
|
entry = self.preferences.setdefault(server_id, {})
|
|
2024
2106
|
entry["enabled"] = True
|
|
2025
2107
|
self._save_preferences()
|
|
2108
|
+
self._trace(server_id, "started successfully")
|
|
2026
2109
|
except Exception as exc:
|
|
2027
2110
|
self.states[server_id] = "error"
|
|
2028
2111
|
self.errors[server_id] = str(exc)
|
|
2112
|
+
self._trace(server_id, f"start failed: {exc}")
|
|
2029
2113
|
if server_id in self.clients:
|
|
2030
2114
|
self.clients.pop(server_id).stop()
|
|
2115
|
+
elif client:
|
|
2116
|
+
client.stop()
|
|
2031
2117
|
return self.list_servers()
|
|
2032
2118
|
|
|
2033
2119
|
def stop(self, server_id: str) -> list[dict]:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|