zerochat 8.0.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.
- {zerochat-8.0.0/zerochat.egg-info → zerochat-8.2.0}/PKG-INFO +1 -1
- {zerochat-8.0.0 → zerochat-8.2.0}/pyproject.toml +1 -1
- {zerochat-8.0.0 → zerochat-8.2.0/zerochat.egg-info}/PKG-INFO +1 -1
- {zerochat-8.0.0 → zerochat-8.2.0}/zerochat.py +83 -47
- {zerochat-8.0.0 → zerochat-8.2.0}/LICENSE +0 -0
- {zerochat-8.0.0 → zerochat-8.2.0}/README.md +0 -0
- {zerochat-8.0.0 → zerochat-8.2.0}/setup.cfg +0 -0
- {zerochat-8.0.0 → zerochat-8.2.0}/zerochat.egg-info/SOURCES.txt +0 -0
- {zerochat-8.0.0 → zerochat-8.2.0}/zerochat.egg-info/dependency_links.txt +0 -0
- {zerochat-8.0.0 → zerochat-8.2.0}/zerochat.egg-info/entry_points.txt +0 -0
- {zerochat-8.0.0 → zerochat-8.2.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.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
|
|
1537
|
-
|
|
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)
|
|
@@ -1757,6 +1778,9 @@ for raw in sys.stdin:
|
|
|
1757
1778
|
"package": "@playwright/mcp",
|
|
1758
1779
|
"version": "0.0.81",
|
|
1759
1780
|
"browser": "chromium"
|
|
1781
|
+
},
|
|
1782
|
+
"requirements": {
|
|
1783
|
+
"node": {"minimumMajor": 18}
|
|
1760
1784
|
}
|
|
1761
1785
|
}, indent=2), encoding="utf-8")
|
|
1762
1786
|
|
|
@@ -1796,43 +1820,9 @@ for raw in sys.stdin:
|
|
|
1796
1820
|
"product": {
|
|
1797
1821
|
"package": "@modelcontextprotocol/server-memory",
|
|
1798
1822
|
"version": "2026.8.31"
|
|
1799
|
-
}
|
|
1800
|
-
}, indent=2), encoding="utf-8")
|
|
1801
|
-
|
|
1802
|
-
# 4. lsp
|
|
1803
|
-
lsp_dir = self.services_root / "lsp"
|
|
1804
|
-
lsp_dir.mkdir(parents=True, exist_ok=True)
|
|
1805
|
-
lsp_service = lsp_dir / "service.json"
|
|
1806
|
-
lsp_installer = lsp_dir / "installer.json"
|
|
1807
|
-
if not lsp_service.exists():
|
|
1808
|
-
lsp_service.write_text(json.dumps({
|
|
1809
|
-
"schemaVersion": 1,
|
|
1810
|
-
"id": "lsp",
|
|
1811
|
-
"displayName": {
|
|
1812
|
-
"es": "LSP y Navegación de Código",
|
|
1813
|
-
"en": "LSP & Code Intelligence"
|
|
1814
|
-
},
|
|
1815
|
-
"description": {
|
|
1816
|
-
"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.",
|
|
1817
|
-
"en": "Language Server Protocol (LSP) server: jump to definitions, symbol search, references, and type inspection without context overload."
|
|
1818
1823
|
},
|
|
1819
|
-
"
|
|
1820
|
-
|
|
1821
|
-
"launch": {
|
|
1822
|
-
"executable": "${nodeExecutable}",
|
|
1823
|
-
"args": ["${serviceDir}/node_modules/@axivo/mcp-lsp/dist/index.js"],
|
|
1824
|
-
"cwd": "${serviceDir}",
|
|
1825
|
-
"env": {},
|
|
1826
|
-
"handshakeTimeoutSeconds": 30
|
|
1827
|
-
}
|
|
1828
|
-
}, indent=2), encoding="utf-8")
|
|
1829
|
-
if not lsp_installer.exists():
|
|
1830
|
-
lsp_installer.write_text(json.dumps({
|
|
1831
|
-
"schemaVersion": 1,
|
|
1832
|
-
"type": "npm",
|
|
1833
|
-
"product": {
|
|
1834
|
-
"package": "@axivo/mcp-lsp",
|
|
1835
|
-
"version": "1.0.5"
|
|
1824
|
+
"requirements": {
|
|
1825
|
+
"node": {"minimumMajor": 18}
|
|
1836
1826
|
}
|
|
1837
1827
|
}, indent=2), encoding="utf-8")
|
|
1838
1828
|
|
|
@@ -1904,6 +1894,7 @@ for raw in sys.stdin:
|
|
|
1904
1894
|
npm = shutil.which("npm") or "npm"
|
|
1905
1895
|
|
|
1906
1896
|
if installer_file.exists():
|
|
1897
|
+
self._trace(server_id, f"reading installer: {installer_file.name}")
|
|
1907
1898
|
marker = service_dir / ".installed.json"
|
|
1908
1899
|
try:
|
|
1909
1900
|
installer = json.loads(installer_file.read_text(encoding="utf-8"))
|
|
@@ -1917,9 +1908,33 @@ for raw in sys.stdin:
|
|
|
1917
1908
|
if not package or not version:
|
|
1918
1909
|
raise RuntimeError("El instalador npm debe definir package y version")
|
|
1919
1910
|
if not shutil.which("node") or not shutil.which("npm"):
|
|
1920
|
-
raise RuntimeError("Node.js
|
|
1911
|
+
raise RuntimeError("Node.js y npm son necesarios para instalar este servicio MCP")
|
|
1912
|
+
|
|
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")
|
|
1924
|
+
version_result = subprocess.run(
|
|
1925
|
+
[node, "--version"], capture_output=True, text=True, timeout=5
|
|
1926
|
+
)
|
|
1927
|
+
version_match = re.match(r"v(\d+)", version_result.stdout.strip())
|
|
1928
|
+
if version_result.returncode != 0 or not version_match:
|
|
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})")
|
|
1931
|
+
if int(version_match.group(1)) < required_node_major:
|
|
1932
|
+
raise RuntimeError(
|
|
1933
|
+
f"{server_id} requiere Node.js {required_node_major}+; se detectó {version_result.stdout.strip()}"
|
|
1934
|
+
)
|
|
1921
1935
|
|
|
1922
|
-
|
|
1936
|
+
package_dir = service_dir / "node_modules" / Path(*package.split("/"))
|
|
1937
|
+
needs_install = not marker.exists() or not package_dir.is_dir()
|
|
1923
1938
|
if not needs_install:
|
|
1924
1939
|
try:
|
|
1925
1940
|
installation = json.loads(marker.read_text(encoding="utf-8"))
|
|
@@ -1930,19 +1945,32 @@ for raw in sys.stdin:
|
|
|
1930
1945
|
|
|
1931
1946
|
if needs_install:
|
|
1932
1947
|
self.states[server_id] = "installing"
|
|
1948
|
+
self._trace(server_id, f"installing npm package: {package}@{version}")
|
|
1933
1949
|
manifest = service_dir / "package.json"
|
|
1934
1950
|
manifest.write_text(json.dumps({"private": True, "dependencies": {package: version}}, indent=2), encoding="utf-8")
|
|
1935
1951
|
res = subprocess.run([npm, "install", "--ignore-scripts", "--no-audit", "--no-fund"], cwd=service_dir, capture_output=True, text=True, timeout=600)
|
|
1936
1952
|
if res.returncode != 0:
|
|
1937
1953
|
raise RuntimeError(f"Fallo instalando dependencias npm: {res.stderr or res.stdout}")
|
|
1954
|
+
self._trace(server_id, f"npm package installed: {package}@{version}")
|
|
1938
1955
|
installation = {"type": "npm", "package": package, "version": version, "nodeExecutable": node}
|
|
1939
1956
|
browser = product.get("browser")
|
|
1940
1957
|
if browser:
|
|
1941
1958
|
playwright_cli = service_dir / "node_modules" / "playwright" / "cli.js"
|
|
1942
|
-
if playwright_cli.is_file():
|
|
1943
|
-
|
|
1944
|
-
|
|
1959
|
+
if not playwright_cli.is_file():
|
|
1960
|
+
raise RuntimeError("No se encontró Playwright después de instalar el servicio MCP")
|
|
1961
|
+
browser_result = subprocess.run(
|
|
1962
|
+
[node, str(playwright_cli), "install", browser], cwd=service_dir,
|
|
1963
|
+
capture_output=True, text=True, timeout=600
|
|
1964
|
+
)
|
|
1965
|
+
if browser_result.returncode != 0:
|
|
1966
|
+
raise RuntimeError(
|
|
1967
|
+
f"Fallo instalando el navegador {browser} de Playwright: "
|
|
1968
|
+
f"{browser_result.stderr or browser_result.stdout}"
|
|
1969
|
+
)
|
|
1970
|
+
installation["browser"] = browser
|
|
1945
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}")
|
|
1946
1974
|
|
|
1947
1975
|
return {
|
|
1948
1976
|
"serviceDir": str(service_dir),
|
|
@@ -1960,6 +1988,8 @@ for raw in sys.stdin:
|
|
|
1960
1988
|
if current and current.running():
|
|
1961
1989
|
return self.list_servers()
|
|
1962
1990
|
self.states[server_id] = "starting"
|
|
1991
|
+
client = None
|
|
1992
|
+
self._trace(server_id, "start requested")
|
|
1963
1993
|
try:
|
|
1964
1994
|
service_dir = server["_directory"]
|
|
1965
1995
|
values = self._prepare_service(server_id, server)
|
|
@@ -1987,7 +2017,10 @@ for raw in sys.stdin:
|
|
|
1987
2017
|
for k, v in launch.get("env", {}).items():
|
|
1988
2018
|
env[k] = self._expand(v, values)
|
|
1989
2019
|
|
|
1990
|
-
client = StdioMcpClient(
|
|
2020
|
+
client = StdioMcpClient(
|
|
2021
|
+
command, args, str(service_dir), env,
|
|
2022
|
+
trace=lambda message: self._trace(server_id, message)
|
|
2023
|
+
)
|
|
1991
2024
|
client.start(int(launch.get("handshakeTimeoutSeconds", 15)))
|
|
1992
2025
|
self.clients[server_id] = client
|
|
1993
2026
|
self.states[server_id] = "running"
|
|
@@ -1995,11 +2028,15 @@ for raw in sys.stdin:
|
|
|
1995
2028
|
entry = self.preferences.setdefault(server_id, {})
|
|
1996
2029
|
entry["enabled"] = True
|
|
1997
2030
|
self._save_preferences()
|
|
2031
|
+
self._trace(server_id, "started successfully")
|
|
1998
2032
|
except Exception as exc:
|
|
1999
2033
|
self.states[server_id] = "error"
|
|
2000
2034
|
self.errors[server_id] = str(exc)
|
|
2035
|
+
self._trace(server_id, f"start failed: {exc}")
|
|
2001
2036
|
if server_id in self.clients:
|
|
2002
2037
|
self.clients.pop(server_id).stop()
|
|
2038
|
+
elif client:
|
|
2039
|
+
client.stop()
|
|
2003
2040
|
return self.list_servers()
|
|
2004
2041
|
|
|
2005
2042
|
def stop(self, server_id: str) -> list[dict]:
|
|
@@ -2064,7 +2101,6 @@ for raw in sys.stdin:
|
|
|
2064
2101
|
|
|
2065
2102
|
|
|
2066
2103
|
GLOBAL_MCP_MANAGER = McpServiceManager()
|
|
2067
|
-
|
|
2068
2104
|
DEFAULT_HEARTBEAT_TIMEOUT = float(os.environ.get("ZEROCHAT_HEARTBEAT_TIMEOUT", "60.0"))
|
|
2069
2105
|
DEFAULT_HEARTBEAT_GRACE = float(os.environ.get("ZEROCHAT_HEARTBEAT_GRACE", "45.0"))
|
|
2070
2106
|
DEFAULT_HEARTBEAT_POLL = float(os.environ.get("ZEROCHAT_HEARTBEAT_POLL", "5.0"))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|