zerochat 8.1.0__tar.gz → 8.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zerochat
3
- Version: 8.1.0
3
+ Version: 8.2.0
4
4
  Summary: Private local web client for AI models, tools and MCP services
5
5
  Author: Alberto Alday
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zerochat"
7
- version = "8.1.0"
7
+ version = "8.2.0"
8
8
  description = "Private local web client for AI models, tools and MCP services"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zerochat
3
- Version: 8.1.0
3
+ Version: 8.2.0
4
4
  Summary: Private local web client for AI models, tools and MCP services
5
5
  Author: Alberto Alday
6
6
  License-Expression: MIT
@@ -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.1.0"
45
+ SOURCE_BACKEND_VERSION = "8.2.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."""
@@ -1488,8 +1488,16 @@ def public_tool_name(server_id: str, original: str) -> str:
1488
1488
  return name
1489
1489
 
1490
1490
 
1491
+ def _mcp_trace_text(value: object, max_len: int = 1000) -> str:
1492
+ """Normaliza diagnósticos MCP para consola sin exponer secretos comunes."""
1493
+ text = " ".join(str(value).strip().splitlines())
1494
+ text = re.sub(r"(?i)(\b(?:api[_-]?key|token|secret|password)\b\s*[=:]\s*)\S+", r"\1***", text)
1495
+ text = re.sub(r"(?i)(--(?:api[_-]?key|token|secret|password)\s+)\S+", r"\1***", text)
1496
+ return text[:max_len - 3] + "..." if len(text) > max_len else text
1497
+
1498
+
1491
1499
  class StdioMcpClient:
1492
- def __init__(self, command: str, args: list[str], cwd: str, env: dict[str, str]):
1500
+ def __init__(self, command: str, args: list[str], cwd: str, env: dict[str, str], trace=None):
1493
1501
  self.command = command
1494
1502
  self.args = args
1495
1503
  self.cwd = cwd
@@ -1500,12 +1508,14 @@ class StdioMcpClient:
1500
1508
  self._lock = threading.Lock()
1501
1509
  self._alive = False
1502
1510
  self.tools: list[dict] = []
1511
+ self._trace = trace or (lambda _message: None)
1503
1512
 
1504
1513
  def running(self) -> bool:
1505
1514
  return self._alive and self.process is not None and self.process.poll() is None
1506
1515
 
1507
1516
  def start(self, handshake_timeout: int = 30):
1508
1517
  cmd = [self.command] + self.args
1518
+ self._trace(f"launching: {_mcp_trace_text(' '.join(cmd))}")
1509
1519
  self.process = subprocess.Popen(
1510
1520
  cmd,
1511
1521
  stdin=subprocess.PIPE,
@@ -1530,11 +1540,14 @@ class StdioMcpClient:
1530
1540
  self.notify("notifications/initialized")
1531
1541
  tools_resp = self.request("tools/list", {}, timeout=10)
1532
1542
  self.tools = tools_resp.get("tools", [])
1543
+ self._trace(f"handshake completed; {len(self.tools)} tool(s) available")
1533
1544
 
1534
1545
  def _drain_stderr(self):
1535
1546
  if self.process and self.process.stderr:
1536
- for _ in self.process.stderr:
1537
- pass
1547
+ for line in self.process.stderr:
1548
+ line = _mcp_trace_text(line)
1549
+ if line:
1550
+ self._trace(f"stderr: {line}")
1538
1551
 
1539
1552
  def _read_stdout(self):
1540
1553
  try:
@@ -1556,6 +1569,10 @@ class StdioMcpClient:
1556
1569
  waiter.put(msg)
1557
1570
  finally:
1558
1571
  self._alive = False
1572
+ if self.process:
1573
+ return_code = self.process.poll()
1574
+ if return_code is not None:
1575
+ self._trace(f"process exited with code {return_code}")
1559
1576
  with self._lock:
1560
1577
  pending = list(self._pending.values())
1561
1578
  self._pending.clear()
@@ -1634,6 +1651,10 @@ class McpServiceManager:
1634
1651
  self.services = self._load_services()
1635
1652
  self.preferences = self._load_preferences()
1636
1653
 
1654
+ @staticmethod
1655
+ def _trace(server_id: str, message: str):
1656
+ console_log(f"[{time.strftime('%H:%M:%S')}] [MCP {server_id}] {_mcp_trace_text(message)}", flush=True)
1657
+
1637
1658
  def _ensure_default_services(self):
1638
1659
  dummy_dir = self.services_root / "dummy_mcp"
1639
1660
  dummy_dir.mkdir(parents=True, exist_ok=True)
@@ -1756,8 +1777,10 @@ for raw in sys.stdin:
1756
1777
  "product": {
1757
1778
  "package": "@playwright/mcp",
1758
1779
  "version": "0.0.81",
1759
- "browser": "chromium",
1760
- "nodeMajor": 18
1780
+ "browser": "chromium"
1781
+ },
1782
+ "requirements": {
1783
+ "node": {"minimumMajor": 18}
1761
1784
  }
1762
1785
  }, indent=2), encoding="utf-8")
1763
1786
 
@@ -1796,46 +1819,10 @@ for raw in sys.stdin:
1796
1819
  "type": "npm",
1797
1820
  "product": {
1798
1821
  "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"
1822
+ "version": "2026.8.31"
1816
1823
  },
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."
1820
- },
1821
- "enabledByDefault": False,
1822
- "transport": "stdio",
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
1824
+ "requirements": {
1825
+ "node": {"minimumMajor": 18}
1839
1826
  }
1840
1827
  }, indent=2), encoding="utf-8")
1841
1828
 
@@ -1907,6 +1894,7 @@ for raw in sys.stdin:
1907
1894
  npm = shutil.which("npm") or "npm"
1908
1895
 
1909
1896
  if installer_file.exists():
1897
+ self._trace(server_id, f"reading installer: {installer_file.name}")
1910
1898
  marker = service_dir / ".installed.json"
1911
1899
  try:
1912
1900
  installer = json.loads(installer_file.read_text(encoding="utf-8"))
@@ -1922,16 +1910,24 @@ for raw in sys.stdin:
1922
1910
  if not shutil.which("node") or not shutil.which("npm"):
1923
1911
  raise RuntimeError("Node.js y npm son necesarios para instalar este servicio MCP")
1924
1912
 
1925
- required_node_major = int(product.get(
1926
- "nodeMajor",
1927
- 24 if package == "@axivo/mcp-lsp" else 18
1928
- ))
1913
+ requirements = installer.get("requirements", {})
1914
+ node_requirement = requirements.get("node")
1915
+ if node_requirement is None:
1916
+ # Compatibilidad con instaladores creados antes de requirements.node.
1917
+ required_node_major = int(product.get("nodeMajor", 18))
1918
+ elif not isinstance(node_requirement, dict):
1919
+ raise RuntimeError("El requisito Node.js debe ser un objeto en installer.json")
1920
+ else:
1921
+ required_node_major = node_requirement.get("minimumMajor")
1922
+ if not isinstance(required_node_major, int) or isinstance(required_node_major, bool) or required_node_major < 1:
1923
+ raise RuntimeError("El requisito Node.js debe definir minimumMajor como un entero positivo")
1929
1924
  version_result = subprocess.run(
1930
1925
  [node, "--version"], capture_output=True, text=True, timeout=5
1931
1926
  )
1932
- version_match = re.match(r"v(\\d+)", version_result.stdout.strip())
1927
+ version_match = re.match(r"v(\d+)", version_result.stdout.strip())
1933
1928
  if version_result.returncode != 0 or not version_match:
1934
1929
  raise RuntimeError("No se pudo comprobar la versión de Node.js necesaria para este servicio MCP")
1930
+ self._trace(server_id, f"Node.js detected: {version_result.stdout.strip()} (minimum {required_node_major})")
1935
1931
  if int(version_match.group(1)) < required_node_major:
1936
1932
  raise RuntimeError(
1937
1933
  f"{server_id} requiere Node.js {required_node_major}+; se detectó {version_result.stdout.strip()}"
@@ -1949,11 +1945,13 @@ for raw in sys.stdin:
1949
1945
 
1950
1946
  if needs_install:
1951
1947
  self.states[server_id] = "installing"
1948
+ self._trace(server_id, f"installing npm package: {package}@{version}")
1952
1949
  manifest = service_dir / "package.json"
1953
1950
  manifest.write_text(json.dumps({"private": True, "dependencies": {package: version}}, indent=2), encoding="utf-8")
1954
1951
  res = subprocess.run([npm, "install", "--ignore-scripts", "--no-audit", "--no-fund"], cwd=service_dir, capture_output=True, text=True, timeout=600)
1955
1952
  if res.returncode != 0:
1956
1953
  raise RuntimeError(f"Fallo instalando dependencias npm: {res.stderr or res.stdout}")
1954
+ self._trace(server_id, f"npm package installed: {package}@{version}")
1957
1955
  installation = {"type": "npm", "package": package, "version": version, "nodeExecutable": node}
1958
1956
  browser = product.get("browser")
1959
1957
  if browser:
@@ -1971,6 +1969,8 @@ for raw in sys.stdin:
1971
1969
  )
1972
1970
  installation["browser"] = browser
1973
1971
  marker.write_text(json.dumps(installation, indent=2), encoding="utf-8")
1972
+ else:
1973
+ self._trace(server_id, f"npm package already installed: {package}@{version}")
1974
1974
 
1975
1975
  return {
1976
1976
  "serviceDir": str(service_dir),
@@ -1988,6 +1988,8 @@ for raw in sys.stdin:
1988
1988
  if current and current.running():
1989
1989
  return self.list_servers()
1990
1990
  self.states[server_id] = "starting"
1991
+ client = None
1992
+ self._trace(server_id, "start requested")
1991
1993
  try:
1992
1994
  service_dir = server["_directory"]
1993
1995
  values = self._prepare_service(server_id, server)
@@ -2015,7 +2017,10 @@ for raw in sys.stdin:
2015
2017
  for k, v in launch.get("env", {}).items():
2016
2018
  env[k] = self._expand(v, values)
2017
2019
 
2018
- client = StdioMcpClient(command, args, str(service_dir), env)
2020
+ client = StdioMcpClient(
2021
+ command, args, str(service_dir), env,
2022
+ trace=lambda message: self._trace(server_id, message)
2023
+ )
2019
2024
  client.start(int(launch.get("handshakeTimeoutSeconds", 15)))
2020
2025
  self.clients[server_id] = client
2021
2026
  self.states[server_id] = "running"
@@ -2023,11 +2028,15 @@ for raw in sys.stdin:
2023
2028
  entry = self.preferences.setdefault(server_id, {})
2024
2029
  entry["enabled"] = True
2025
2030
  self._save_preferences()
2031
+ self._trace(server_id, "started successfully")
2026
2032
  except Exception as exc:
2027
2033
  self.states[server_id] = "error"
2028
2034
  self.errors[server_id] = str(exc)
2035
+ self._trace(server_id, f"start failed: {exc}")
2029
2036
  if server_id in self.clients:
2030
2037
  self.clients.pop(server_id).stop()
2038
+ elif client:
2039
+ client.stop()
2031
2040
  return self.list_servers()
2032
2041
 
2033
2042
  def stop(self, server_id: str) -> list[dict]:
File without changes
File without changes
File without changes