zerochat 8.2.0__tar.gz → 8.4.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.2.0/zerochat.egg-info → zerochat-8.4.0}/PKG-INFO +1 -1
- {zerochat-8.2.0 → zerochat-8.4.0}/pyproject.toml +1 -1
- {zerochat-8.2.0 → zerochat-8.4.0/zerochat.egg-info}/PKG-INFO +1 -1
- {zerochat-8.2.0 → zerochat-8.4.0}/zerochat.py +66 -217
- {zerochat-8.2.0 → zerochat-8.4.0}/LICENSE +0 -0
- {zerochat-8.2.0 → zerochat-8.4.0}/README.md +0 -0
- {zerochat-8.2.0 → zerochat-8.4.0}/setup.cfg +0 -0
- {zerochat-8.2.0 → zerochat-8.4.0}/zerochat.egg-info/SOURCES.txt +0 -0
- {zerochat-8.2.0 → zerochat-8.4.0}/zerochat.egg-info/dependency_links.txt +0 -0
- {zerochat-8.2.0 → zerochat-8.4.0}/zerochat.egg-info/entry_points.txt +0 -0
- {zerochat-8.2.0 → zerochat-8.4.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.4.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:
|
|
@@ -377,6 +388,22 @@ def check_version():
|
|
|
377
388
|
# Modo offline o timeout ignorado de forma segura
|
|
378
389
|
pass
|
|
379
390
|
# ==============================================================================
|
|
391
|
+
# Generated from services/ by scripts/build-managed-services.mjs. Do not edit.
|
|
392
|
+
# ==============================================================================
|
|
393
|
+
|
|
394
|
+
import json
|
|
395
|
+
|
|
396
|
+
MANAGED_SERVICE_FILES: dict[str, str] = json.loads("{\n \"composio/installer.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"mcp-remote\\\",\\n \\\"version\\\": \\\"0.13.5\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"composio/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"composio\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Composio Connect\\\",\\n \\\"en\\\": \\\"Composio Connect\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"MCP remoto para acceder a aplicaciones como Gmail, Google Drive, Slack o GitHub. Composio solicita la autorización de cada aplicación cuando sea necesaria.\\\",\\n \\\"en\\\": \\\"Remote MCP for apps such as Gmail, Google Drive, Slack, and GitHub. Composio requests authorization for each app when it is needed.\\\"\\n },\\n \\\"help\\\": {\\n \\\"url\\\": \\\"https://docs.composio.dev/docs/composio-connect\\\",\\n \\\"label\\\": {\\n \\\"es\\\": \\\"Guía oficial de Composio Connect\\\",\\n \\\"en\\\": \\\"Composio Connect official guide\\\"\\n }\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"remote\\\": {\\n \\\"type\\\": \\\"mcp-remote\\\",\\n \\\"transport\\\": \\\"streamable-http\\\",\\n \\\"url\\\": \\\"https://connect.composio.dev/mcp\\\",\\n \\\"authentication\\\": \\\"composio-connect\\\"\\n },\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${nodeExecutable}\\\",\\n \\\"args\\\": [\\n \\\"${serviceDir}/node_modules/mcp-remote/dist/proxy.js\\\",\\n \\\"https://connect.composio.dev/mcp\\\",\\n \\\"--protocol\\\",\\n \\\"legacy\\\",\\n \\\"--static-oauth-client-metadata\\\",\\n \\\"{\\\\\\\"client_name\\\\\\\":\\\\\\\"zerochat-mcp-remote\\\\\\\"}\\\",\\n \\\"--auth-timeout\\\",\\n \\\"120\\\"\\n ],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {\\n \\\"HOME\\\": \\\"${serviceDir}\\\",\\n \\\"USERPROFILE\\\": \\\"${serviceDir}\\\"\\n },\\n \\\"handshakeTimeoutSeconds\\\": 150\\n }\\n}\\n\",\n \"ejemplo/ejemplo_mcp_server.py\": \"#!/usr/bin/env python3\\nimport json\\nimport sys\\n\\ndef reply(req_id, result=None, error=None):\\n resp = {\\\"jsonrpc\\\": \\\"2.0\\\", \\\"id\\\": req_id}\\n if error:\\n resp[\\\"error\\\"] = error\\n else:\\n resp[\\\"result\\\"] = result\\n sys.stdout.write(json.dumps(resp) + \\\"\\\\n\\\")\\n sys.stdout.flush()\\n\\nfor raw in sys.stdin:\\n try:\\n req = json.loads(raw)\\n except Exception:\\n continue\\n req_id = req.get(\\\"id\\\")\\n method = req.get(\\\"method\\\")\\n params = req.get(\\\"params\\\", {})\\n if method == \\\"initialize\\\":\\n reply(req_id, {\\n \\\"protocolVersion\\\": \\\"2024-11-05\\\",\\n \\\"serverInfo\\\": {\\\"name\\\": \\\"ZeroChat MCP Example\\\", \\\"version\\\": \\\"1.0.0\\\"},\\n \\\"capabilities\\\": {\\\"tools\\\": {}}\\n })\\n elif method == \\\"tools/list\\\":\\n reply(req_id, {\\\"tools\\\": [{\\n \\\"name\\\": \\\"echo\\\",\\n \\\"description\\\": \\\"Echo back a message for testing.\\\",\\n \\\"inputSchema\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"properties\\\": {\\\"message\\\": {\\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"Message to echo.\\\"}},\\n \\\"required\\\": [\\\"message\\\"]\\n }\\n }]})\\n elif method == \\\"tools/call\\\":\\n if params.get(\\\"name\\\") != \\\"echo\\\":\\n reply(req_id, error={\\\"code\\\": -32601, \\\"message\\\": \\\"Tool not found\\\"})\\n continue\\n msg = params.get(\\\"arguments\\\", {}).get(\\\"message\\\", \\\"\\\")\\n reply(req_id, {\\n \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": f\\\"echo: {msg}\\\"}],\\n \\\"isError\\\": False\\n })\\n\",\n \"ejemplo/installer.json.example\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"@scope/mcp-package\\\",\\n \\\"version\\\": \\\"1.2.3\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"ejemplo/README.md\": \"# Crear un MCP gestionado por ZeroChat\\n\\nEsta 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.\\n\\n## Procedimiento obligatorio\\n\\n1. Lee `README.md`, `service.json` y `installer.json.example` de la carpeta `ejemplo` situada en el directorio de datos de ZeroChat.\\n2. Consulta solo la documentación oficial del MCP solicitado.\\n3. Crea una carpeta con un identificador seguro y descriptivo junto a `ejemplo`.\\n4. Crea `service.json` copiando esta estructura y adapta únicamente los valores necesarios.\\n5. 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.\\n6. Añade `help.url` con la página oficial de instalación y etiquetas en español e inglés.\\n7. No incluyas tokens, contraseñas ni valores privados. Decláralos en `launch.env` con un marcador, por ejemplo `${HOME_ASSISTANT_TOKEN}`.\\n8. No inicies el servicio. Al terminar, informa de los archivos creados, fuentes oficiales y requisitos pendientes.\\n\\n## Límites de ZeroChat\\n\\nZeroChat 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`.\\n\\nZeroChat también puede usar `mcp-remote` como puente para un MCP remoto HTTP/SSE con OAuth. Decláralo como un servicio `stdio`: el proceso local debe ser `mcp-remote`, mientras que `remote` describe el endpoint remoto para la interfaz. Conserva las credenciales y el almacén OAuth dentro de `${serviceDir}`.\\n\\n## Contratos JSON\\n\\n- `service.json` es obligatorio. Conserva `schemaVersion`, `id`, textos bilingües, `transport: \\\"stdio\\\"` y `launch`.\\n- En `launch`, usa `${serviceDir}`, `${nodeExecutable}` o `${pythonExecutable}`; no uses rutas absolutas de usuario.\\n- `installer.json` es opcional y hoy solo admite `type: \\\"npm\\\"`.\\n- `help` es opcional, pero debe incluirse en todo servicio creado:\\n\\n```json\\n\\\"help\\\": {\\n \\\"url\\\": \\\"https://documentacion-oficial.example/install\\\",\\n \\\"label\\\": { \\\"es\\\": \\\"Instalación oficial\\\", \\\"en\\\": \\\"Official installation\\\" }\\n}\\n```\\n\\n## Prompt para el usuario\\n\\nSustituye únicamente `<nombre-del-mcp>`:\\n\\n```text\\nPara instalar el MCP <nombre-del-mcp>, lee primero services/ejemplo/README.md y sigue exactamente sus instrucciones.\\n```\\n\",\n \"ejemplo/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"ejemplo\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Ejemplo de MCP\\\",\\n \\\"en\\\": \\\"MCP example\\\"\\n },\\n \\\"description\\\": {\\n \\\"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.\\\",\\n \\\"en\\\": \\\"Minimal template for creating ZeroChat-managed MCP services. It demonstrates the stdio protocol and configuration structure for a new MCP.\\\"\\n },\\n \\\"help\\\": {\\n \\\"url\\\": \\\"help/mcp.html#crear-mcp-con-agente\\\",\\n \\\"label\\\": {\\n \\\"es\\\": \\\"Guía para crear un MCP gestionado\\\",\\n \\\"en\\\": \\\"Guide to creating a managed MCP\\\"\\n }\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${pythonExecutable}\\\",\\n \\\"args\\\": [\\\"${serviceDir}/ejemplo_mcp_server.py\\\"],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {},\\n \\\"handshakeTimeoutSeconds\\\": 10\\n }\\n}\\n\",\n \"memory/installer.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"@modelcontextprotocol/server-memory\\\",\\n \\\"version\\\": \\\"2026.8.31\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"memory/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"memory\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Memoria y Grafos (Knowledge Graph)\\\",\\n \\\"en\\\": \\\"Memory & Knowledge Graph\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Almacenamiento persistente de entidades, preferencias y contexto histórico estructurado en un grafo de conocimiento.\\\",\\n \\\"en\\\": \\\"Persistent storage of entities, preferences, and historical context structured as a knowledge graph.\\\"\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${nodeExecutable}\\\",\\n \\\"args\\\": [\\\"${serviceDir}/node_modules/@modelcontextprotocol/server-memory/dist/index.js\\\"],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {\\n \\\"MEMORY_FILE_PATH\\\": \\\"${serviceDir}/memory.jsonl\\\"\\n },\\n \\\"handshakeTimeoutSeconds\\\": 30\\n }\\n}\\n\\n\",\n \"playwright/installer.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"@playwright/mcp\\\",\\n \\\"version\\\": \\\"0.0.81\\\",\\n \\\"browser\\\": \\\"chromium\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"playwright/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"playwright\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Playwright MCP\\\",\\n \\\"en\\\": \\\"Playwright MCP\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Automatización de navegador mediante el servidor MCP oficial de Playwright.\\\",\\n \\\"en\\\": \\\"Browser automation through the official Playwright MCP server.\\\"\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${nodeExecutable}\\\",\\n \\\"args\\\": [\\\"${serviceDir}/node_modules/@playwright/mcp/cli.js\\\", \\\"--browser=chromium\\\"],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {},\\n \\\"handshakeTimeoutSeconds\\\": 30\\n },\\n \\\"options\\\": [\\n {\\n \\\"id\\\": \\\"headless\\\",\\n \\\"type\\\": \\\"boolean\\\",\\n \\\"label\\\": {\\n \\\"es\\\": \\\"Navegación en segundo plano (Headless)\\\",\\n \\\"en\\\": \\\"Headless background mode\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Desactívalo para ver la ventana del navegador durante la automatización\\\",\\n \\\"en\\\": \\\"Disable to display the browser window during automation\\\"\\n },\\n \\\"default\\\": true,\\n \\\"argsWhenTrue\\\": [\\\"--headless\\\"],\\n \\\"argsWhenFalse\\\": []\\n }\\n ]\\n}\\n\\n\"\n}")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def materialize_managed_services(services_root: Path | None = None) -> None:
|
|
400
|
+
"""Installs the managed service files without deleting user data or services."""
|
|
401
|
+
root = Path(services_root) if services_root else get_data_dir() / "services"
|
|
402
|
+
for relative_path, content in MANAGED_SERVICE_FILES.items():
|
|
403
|
+
destination = root / relative_path
|
|
404
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
405
|
+
destination.write_text(content, encoding="utf-8")
|
|
406
|
+
# ==============================================================================
|
|
380
407
|
# Herramientas Locales Core
|
|
381
408
|
# ==============================================================================
|
|
382
409
|
|
|
@@ -1480,9 +1507,11 @@ def public_tool_name(server_id: str, original: str) -> str:
|
|
|
1480
1507
|
def encode(value: str, tool: bool = False) -> str:
|
|
1481
1508
|
if not isinstance(value, str) or not value or len(value) > 256:
|
|
1482
1509
|
raise ValueError("Componente de nombre MCP no válido")
|
|
1483
|
-
|
|
1510
|
+
# MCP function names preserve ASCII case, which keeps remote tool
|
|
1511
|
+
# names readable while still escaping unsupported characters.
|
|
1512
|
+
return "".join(ch if ("a" <= ch <= "z" or "A" <= ch <= "Z" or "0" <= ch <= "9" or (tool and ch == "_"))
|
|
1484
1513
|
else f"z{ord(ch):x}z" for ch in value)
|
|
1485
|
-
name = f"
|
|
1514
|
+
name = f"{encode(server_id)}_{encode(original, True)}"
|
|
1486
1515
|
if len(name) > 64:
|
|
1487
1516
|
raise ValueError(f"El nombre público de la herramienta MCP excede 64 caracteres: {name}")
|
|
1488
1517
|
return name
|
|
@@ -1493,6 +1522,7 @@ def _mcp_trace_text(value: object, max_len: int = 1000) -> str:
|
|
|
1493
1522
|
text = " ".join(str(value).strip().splitlines())
|
|
1494
1523
|
text = re.sub(r"(?i)(\b(?:api[_-]?key|token|secret|password)\b\s*[=:]\s*)\S+", r"\1***", text)
|
|
1495
1524
|
text = re.sub(r"(?i)(--(?:api[_-]?key|token|secret|password)\s+)\S+", r"\1***", text)
|
|
1525
|
+
text = re.sub(r"(?i)([?&](?:access_token|code|id_token|refresh_token|state)=)[^&\s]+", r"\1***", text)
|
|
1496
1526
|
return text[:max_len - 3] + "..." if len(text) > max_len else text
|
|
1497
1527
|
|
|
1498
1528
|
|
|
@@ -1635,11 +1665,7 @@ class McpServiceManager:
|
|
|
1635
1665
|
if services_root:
|
|
1636
1666
|
self.services_root = Path(services_root)
|
|
1637
1667
|
else:
|
|
1638
|
-
|
|
1639
|
-
Path.cwd() / "services",
|
|
1640
|
-
get_data_dir() / "services"
|
|
1641
|
-
]
|
|
1642
|
-
self.services_root = candidates[0] if get_dev_root() is not None and candidates[0].is_dir() else candidates[1]
|
|
1668
|
+
self.services_root = get_data_dir() / "services"
|
|
1643
1669
|
self.services_root.mkdir(parents=True, exist_ok=True)
|
|
1644
1670
|
self.config_file = get_data_dir() / "config" / "services.json"
|
|
1645
1671
|
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -1647,7 +1673,7 @@ class McpServiceManager:
|
|
|
1647
1673
|
self.states: dict[str, str] = {}
|
|
1648
1674
|
self.errors: dict[str, str] = {}
|
|
1649
1675
|
self._lock = threading.Lock()
|
|
1650
|
-
self.
|
|
1676
|
+
materialize_managed_services(self.services_root)
|
|
1651
1677
|
self.services = self._load_services()
|
|
1652
1678
|
self.preferences = self._load_preferences()
|
|
1653
1679
|
|
|
@@ -1655,177 +1681,6 @@ class McpServiceManager:
|
|
|
1655
1681
|
def _trace(server_id: str, message: str):
|
|
1656
1682
|
console_log(f"[{time.strftime('%H:%M:%S')}] [MCP {server_id}] {_mcp_trace_text(message)}", flush=True)
|
|
1657
1683
|
|
|
1658
|
-
def _ensure_default_services(self):
|
|
1659
|
-
dummy_dir = self.services_root / "dummy_mcp"
|
|
1660
|
-
dummy_dir.mkdir(parents=True, exist_ok=True)
|
|
1661
|
-
service_json_file = dummy_dir / "service.json"
|
|
1662
|
-
dummy_server_file = dummy_dir / "dummy_mcp_server.py"
|
|
1663
|
-
|
|
1664
|
-
if not service_json_file.exists():
|
|
1665
|
-
service_json_file.write_text(json.dumps({
|
|
1666
|
-
"schemaVersion": 1,
|
|
1667
|
-
"id": "dummy_mcp",
|
|
1668
|
-
"displayName": {
|
|
1669
|
-
"es": "MCP de prueba",
|
|
1670
|
-
"en": "Test MCP"
|
|
1671
|
-
},
|
|
1672
|
-
"description": {
|
|
1673
|
-
"es": "Servicio MCP mínimo para comprobar la infraestructura externa.",
|
|
1674
|
-
"en": "Minimal MCP service for verifying the external infrastructure."
|
|
1675
|
-
},
|
|
1676
|
-
"enabledByDefault": False,
|
|
1677
|
-
"transport": "stdio",
|
|
1678
|
-
"launch": {
|
|
1679
|
-
"executable": "${pythonExecutable}",
|
|
1680
|
-
"args": ["${serviceDir}/dummy_mcp_server.py"],
|
|
1681
|
-
"cwd": "${serviceDir}",
|
|
1682
|
-
"env": {},
|
|
1683
|
-
"handshakeTimeoutSeconds": 10
|
|
1684
|
-
}
|
|
1685
|
-
}, indent=2), encoding="utf-8")
|
|
1686
|
-
|
|
1687
|
-
if not dummy_server_file.exists():
|
|
1688
|
-
dummy_server_file.write_text('''#!/usr/bin/env python3
|
|
1689
|
-
import json, sys
|
|
1690
|
-
|
|
1691
|
-
def reply(req_id, result=None, error=None):
|
|
1692
|
-
resp = {"jsonrpc": "2.0", "id": req_id}
|
|
1693
|
-
if error: resp["error"] = error
|
|
1694
|
-
else: resp["result"] = result
|
|
1695
|
-
sys.stdout.write(json.dumps(resp) + "\\n")
|
|
1696
|
-
sys.stdout.flush()
|
|
1697
|
-
|
|
1698
|
-
for raw in sys.stdin:
|
|
1699
|
-
try: req = json.loads(raw)
|
|
1700
|
-
except: continue
|
|
1701
|
-
req_id, method, params = req.get("id"), req.get("method"), req.get("params", {})
|
|
1702
|
-
if method == "initialize":
|
|
1703
|
-
reply(req_id, {
|
|
1704
|
-
"protocolVersion": "2024-11-05",
|
|
1705
|
-
"serverInfo": {"name": "ZeroChat Dummy MCP", "version": "1.0.0"},
|
|
1706
|
-
"capabilities": {"tools": {}}
|
|
1707
|
-
})
|
|
1708
|
-
elif method == "tools/list":
|
|
1709
|
-
reply(req_id, {"tools": [{
|
|
1710
|
-
"name": "echo",
|
|
1711
|
-
"description": "Echo back a message for testing.",
|
|
1712
|
-
"inputSchema": {
|
|
1713
|
-
"type": "object",
|
|
1714
|
-
"properties": {"message": {"type": "string", "description": "Message to echo."}},
|
|
1715
|
-
"required": ["message"]
|
|
1716
|
-
}
|
|
1717
|
-
}]})
|
|
1718
|
-
elif method == "tools/call":
|
|
1719
|
-
if params.get("name") != "echo":
|
|
1720
|
-
reply(req_id, error={"code": -32601, "message": "Tool not found"})
|
|
1721
|
-
continue
|
|
1722
|
-
msg = params.get("arguments", {}).get("message", "")
|
|
1723
|
-
reply(req_id, {
|
|
1724
|
-
"content": [{"type": "text", "text": f"echo: {msg}"}],
|
|
1725
|
-
"isError": False
|
|
1726
|
-
})
|
|
1727
|
-
''', encoding="utf-8")
|
|
1728
|
-
|
|
1729
|
-
# 2. playwright
|
|
1730
|
-
playwright_dir = self.services_root / "playwright"
|
|
1731
|
-
playwright_dir.mkdir(parents=True, exist_ok=True)
|
|
1732
|
-
pw_service = playwright_dir / "service.json"
|
|
1733
|
-
pw_installer = playwright_dir / "installer.json"
|
|
1734
|
-
if not pw_service.exists():
|
|
1735
|
-
pw_service.write_text(json.dumps({
|
|
1736
|
-
"schemaVersion": 1,
|
|
1737
|
-
"id": "playwright",
|
|
1738
|
-
"displayName": {
|
|
1739
|
-
"es": "Playwright MCP",
|
|
1740
|
-
"en": "Playwright MCP"
|
|
1741
|
-
},
|
|
1742
|
-
"description": {
|
|
1743
|
-
"es": "Automatización de navegador mediante el servidor MCP oficial de Playwright.",
|
|
1744
|
-
"en": "Browser automation through the official Playwright MCP server."
|
|
1745
|
-
},
|
|
1746
|
-
"enabledByDefault": False,
|
|
1747
|
-
"transport": "stdio",
|
|
1748
|
-
"launch": {
|
|
1749
|
-
"executable": "${nodeExecutable}",
|
|
1750
|
-
"args": ["${serviceDir}/node_modules/@playwright/mcp/cli.js", "--browser=chromium"],
|
|
1751
|
-
"cwd": "${serviceDir}",
|
|
1752
|
-
"env": {},
|
|
1753
|
-
"handshakeTimeoutSeconds": 30
|
|
1754
|
-
},
|
|
1755
|
-
"options": [
|
|
1756
|
-
{
|
|
1757
|
-
"id": "headless",
|
|
1758
|
-
"type": "boolean",
|
|
1759
|
-
"label": {
|
|
1760
|
-
"es": "Navegación en segundo plano (Headless)",
|
|
1761
|
-
"en": "Headless background mode"
|
|
1762
|
-
},
|
|
1763
|
-
"description": {
|
|
1764
|
-
"es": "Desactívalo para ver la ventana del navegador durante la automatización",
|
|
1765
|
-
"en": "Disable to display the browser window during automation"
|
|
1766
|
-
},
|
|
1767
|
-
"default": True,
|
|
1768
|
-
"argsWhenTrue": ["--headless"],
|
|
1769
|
-
"argsWhenFalse": []
|
|
1770
|
-
}
|
|
1771
|
-
]
|
|
1772
|
-
}, indent=2), encoding="utf-8")
|
|
1773
|
-
if not pw_installer.exists():
|
|
1774
|
-
pw_installer.write_text(json.dumps({
|
|
1775
|
-
"schemaVersion": 1,
|
|
1776
|
-
"type": "npm",
|
|
1777
|
-
"product": {
|
|
1778
|
-
"package": "@playwright/mcp",
|
|
1779
|
-
"version": "0.0.81",
|
|
1780
|
-
"browser": "chromium"
|
|
1781
|
-
},
|
|
1782
|
-
"requirements": {
|
|
1783
|
-
"node": {"minimumMajor": 18}
|
|
1784
|
-
}
|
|
1785
|
-
}, indent=2), encoding="utf-8")
|
|
1786
|
-
|
|
1787
|
-
# 3. memory
|
|
1788
|
-
memory_dir = self.services_root / "memory"
|
|
1789
|
-
memory_dir.mkdir(parents=True, exist_ok=True)
|
|
1790
|
-
mem_service = memory_dir / "service.json"
|
|
1791
|
-
mem_installer = memory_dir / "installer.json"
|
|
1792
|
-
if not mem_service.exists():
|
|
1793
|
-
mem_service.write_text(json.dumps({
|
|
1794
|
-
"schemaVersion": 1,
|
|
1795
|
-
"id": "memory",
|
|
1796
|
-
"displayName": {
|
|
1797
|
-
"es": "Memoria y Grafos (Knowledge Graph)",
|
|
1798
|
-
"en": "Memory & Knowledge Graph"
|
|
1799
|
-
},
|
|
1800
|
-
"description": {
|
|
1801
|
-
"es": "Almacenamiento persistente de entidades, preferencias y contexto histórico estructurado en un grafo de conocimiento.",
|
|
1802
|
-
"en": "Persistent storage of entities, preferences, and historical context structured as a knowledge graph."
|
|
1803
|
-
},
|
|
1804
|
-
"enabledByDefault": False,
|
|
1805
|
-
"transport": "stdio",
|
|
1806
|
-
"launch": {
|
|
1807
|
-
"executable": "${nodeExecutable}",
|
|
1808
|
-
"args": ["${serviceDir}/node_modules/@modelcontextprotocol/server-memory/dist/index.js"],
|
|
1809
|
-
"cwd": "${serviceDir}",
|
|
1810
|
-
"env": {
|
|
1811
|
-
"MEMORY_FILE_PATH": "${serviceDir}/memory.jsonl"
|
|
1812
|
-
},
|
|
1813
|
-
"handshakeTimeoutSeconds": 30
|
|
1814
|
-
}
|
|
1815
|
-
}, indent=2), encoding="utf-8")
|
|
1816
|
-
if not mem_installer.exists():
|
|
1817
|
-
mem_installer.write_text(json.dumps({
|
|
1818
|
-
"schemaVersion": 1,
|
|
1819
|
-
"type": "npm",
|
|
1820
|
-
"product": {
|
|
1821
|
-
"package": "@modelcontextprotocol/server-memory",
|
|
1822
|
-
"version": "2026.8.31"
|
|
1823
|
-
},
|
|
1824
|
-
"requirements": {
|
|
1825
|
-
"node": {"minimumMajor": 18}
|
|
1826
|
-
}
|
|
1827
|
-
}, indent=2), encoding="utf-8")
|
|
1828
|
-
|
|
1829
1684
|
def _load_services(self) -> dict[str, dict]:
|
|
1830
1685
|
servers = {}
|
|
1831
1686
|
for directory in sorted(self.services_root.iterdir()):
|
|
@@ -1871,6 +1726,8 @@ for raw in sys.stdin:
|
|
|
1871
1726
|
"id": server_id,
|
|
1872
1727
|
"displayName": server.get("displayName", {}),
|
|
1873
1728
|
"description": server.get("description", {}),
|
|
1729
|
+
"help": server.get("help"),
|
|
1730
|
+
"remote": server.get("remote"),
|
|
1874
1731
|
"enabled": pref.get("enabled", server.get("enabledByDefault", False)),
|
|
1875
1732
|
"status": "running" if running else self.states.get(server_id, "stopped"),
|
|
1876
1733
|
"toolCount": len(client.tools) if running else 0,
|
|
@@ -2335,7 +2192,10 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2335
2192
|
self.send_header("Content-Length", str(len(body)))
|
|
2336
2193
|
self.send_cors_headers()
|
|
2337
2194
|
self.end_headers()
|
|
2338
|
-
|
|
2195
|
+
try:
|
|
2196
|
+
self.wfile.write(body)
|
|
2197
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
2198
|
+
self.close_connection = True
|
|
2339
2199
|
|
|
2340
2200
|
def do_GET(self):
|
|
2341
2201
|
t0 = time.monotonic()
|
|
@@ -2660,21 +2520,6 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2660
2520
|
"content": [{"type": "text", "text": json.dumps({"success": False, "error": str(ex)}, ensure_ascii=False)}],
|
|
2661
2521
|
"isError": True
|
|
2662
2522
|
}
|
|
2663
|
-
elif tool_name.startswith("mcp_"):
|
|
2664
|
-
try:
|
|
2665
|
-
result = GLOBAL_MCP_MANAGER.call(tool_name, tool_args)
|
|
2666
|
-
if isinstance(result, dict) and result.get("isError"):
|
|
2667
|
-
c_list = result.get("content", [])
|
|
2668
|
-
if c_list and isinstance(c_list, list) and isinstance(c_list[0], dict):
|
|
2669
|
-
tool_error_info = c_list[0].get("text", "Error en herramienta MCP")
|
|
2670
|
-
else:
|
|
2671
|
-
tool_error_info = "Error en herramienta MCP"
|
|
2672
|
-
except Exception as ex:
|
|
2673
|
-
tool_error_info = str(ex)
|
|
2674
|
-
result = {
|
|
2675
|
-
"content": [{"type": "text", "text": json.dumps({"success": False, "error": str(ex)}, ensure_ascii=False)}],
|
|
2676
|
-
"isError": True
|
|
2677
|
-
}
|
|
2678
2523
|
else:
|
|
2679
2524
|
error = {"code": -32601, "message": f"Herramienta local '{tool_name}' no encontrada."}
|
|
2680
2525
|
elif method == "zerochat/external/status":
|
|
@@ -2716,7 +2561,11 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2716
2561
|
self.send_header("Content-Length", str(len(resp_bytes)))
|
|
2717
2562
|
self.send_cors_headers()
|
|
2718
2563
|
self.end_headers()
|
|
2719
|
-
|
|
2564
|
+
try:
|
|
2565
|
+
self.wfile.write(resp_bytes)
|
|
2566
|
+
except (BrokenPipeError, ConnectionResetError):
|
|
2567
|
+
self.close_connection = True
|
|
2568
|
+
return
|
|
2720
2569
|
self._log_res(200, f"{safe_path} {action_tag}", (time.monotonic() - t0) * 1000, error_info=error_info)
|
|
2721
2570
|
|
|
2722
2571
|
def log_message(self, format, *args):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|