zerochat 8.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zerochat
3
- Version: 8.3.0
3
+ Version: 8.4.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.3.0"
7
+ version = "8.4.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.3.0
3
+ Version: 8.4.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.3.0"
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."""
@@ -388,6 +388,22 @@ def check_version():
388
388
  # Modo offline o timeout ignorado de forma segura
389
389
  pass
390
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
+ # ==============================================================================
391
407
  # Herramientas Locales Core
392
408
  # ==============================================================================
393
409
 
@@ -1491,9 +1507,11 @@ def public_tool_name(server_id: str, original: str) -> str:
1491
1507
  def encode(value: str, tool: bool = False) -> str:
1492
1508
  if not isinstance(value, str) or not value or len(value) > 256:
1493
1509
  raise ValueError("Componente de nombre MCP no válido")
1494
- return "".join(ch if ("a" <= ch <= "y" or "0" <= ch <= "9" or (tool and ch == "_"))
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 == "_"))
1495
1513
  else f"z{ord(ch):x}z" for ch in value)
1496
- name = f"mcp_{encode(server_id)}_{encode(original, True)}"
1514
+ name = f"{encode(server_id)}_{encode(original, True)}"
1497
1515
  if len(name) > 64:
1498
1516
  raise ValueError(f"El nombre público de la herramienta MCP excede 64 caracteres: {name}")
1499
1517
  return name
@@ -1504,6 +1522,7 @@ def _mcp_trace_text(value: object, max_len: int = 1000) -> str:
1504
1522
  text = " ".join(str(value).strip().splitlines())
1505
1523
  text = re.sub(r"(?i)(\b(?:api[_-]?key|token|secret|password)\b\s*[=:]\s*)\S+", r"\1***", text)
1506
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)
1507
1526
  return text[:max_len - 3] + "..." if len(text) > max_len else text
1508
1527
 
1509
1528
 
@@ -1654,7 +1673,7 @@ class McpServiceManager:
1654
1673
  self.states: dict[str, str] = {}
1655
1674
  self.errors: dict[str, str] = {}
1656
1675
  self._lock = threading.Lock()
1657
- self._ensure_default_services()
1676
+ materialize_managed_services(self.services_root)
1658
1677
  self.services = self._load_services()
1659
1678
  self.preferences = self._load_preferences()
1660
1679
 
@@ -1662,246 +1681,6 @@ class McpServiceManager:
1662
1681
  def _trace(server_id: str, message: str):
1663
1682
  console_log(f"[{time.strftime('%H:%M:%S')}] [MCP {server_id}] {_mcp_trace_text(message)}", flush=True)
1664
1683
 
1665
- def _ensure_default_services(self):
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"
1672
-
1673
- if not service_json_file.exists():
1674
- service_json_file.write_text(json.dumps({
1675
- "schemaVersion": 1,
1676
- "id": "ejemplo",
1677
- "displayName": {
1678
- "es": "Ejemplo de MCP",
1679
- "en": "MCP example"
1680
- },
1681
- "description": {
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
- }
1691
- },
1692
- "enabledByDefault": False,
1693
- "transport": "stdio",
1694
- "launch": {
1695
- "executable": "${pythonExecutable}",
1696
- "args": ["${serviceDir}/ejemplo_mcp_server.py"],
1697
- "cwd": "${serviceDir}",
1698
- "env": {},
1699
- "handshakeTimeoutSeconds": 10
1700
- }
1701
- }, indent=2), encoding="utf-8")
1702
-
1703
- if not example_server_file.exists():
1704
- example_server_file.write_text('''#!/usr/bin/env python3
1705
- import json, sys
1706
-
1707
- def reply(req_id, result=None, error=None):
1708
- resp = {"jsonrpc": "2.0", "id": req_id}
1709
- if error: resp["error"] = error
1710
- else: resp["result"] = result
1711
- sys.stdout.write(json.dumps(resp) + "\\n")
1712
- sys.stdout.flush()
1713
-
1714
- for raw in sys.stdin:
1715
- try: req = json.loads(raw)
1716
- except: continue
1717
- req_id, method, params = req.get("id"), req.get("method"), req.get("params", {})
1718
- if method == "initialize":
1719
- reply(req_id, {
1720
- "protocolVersion": "2024-11-05",
1721
- "serverInfo": {"name": "ZeroChat MCP Example", "version": "1.0.0"},
1722
- "capabilities": {"tools": {}}
1723
- })
1724
- elif method == "tools/list":
1725
- reply(req_id, {"tools": [{
1726
- "name": "echo",
1727
- "description": "Echo back a message for testing.",
1728
- "inputSchema": {
1729
- "type": "object",
1730
- "properties": {"message": {"type": "string", "description": "Message to echo."}},
1731
- "required": ["message"]
1732
- }
1733
- }]})
1734
- elif method == "tools/call":
1735
- if params.get("name") != "echo":
1736
- reply(req_id, error={"code": -32601, "message": "Tool not found"})
1737
- continue
1738
- msg = params.get("arguments", {}).get("message", "")
1739
- reply(req_id, {
1740
- "content": [{"type": "text", "text": f"echo: {msg}"}],
1741
- "isError": False
1742
- })
1743
- ''', encoding="utf-8")
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
-
1805
- # 2. playwright
1806
- playwright_dir = self.services_root / "playwright"
1807
- playwright_dir.mkdir(parents=True, exist_ok=True)
1808
- pw_service = playwright_dir / "service.json"
1809
- pw_installer = playwright_dir / "installer.json"
1810
- if not pw_service.exists():
1811
- pw_service.write_text(json.dumps({
1812
- "schemaVersion": 1,
1813
- "id": "playwright",
1814
- "displayName": {
1815
- "es": "Playwright MCP",
1816
- "en": "Playwright MCP"
1817
- },
1818
- "description": {
1819
- "es": "Automatización de navegador mediante el servidor MCP oficial de Playwright.",
1820
- "en": "Browser automation through the official Playwright MCP server."
1821
- },
1822
- "enabledByDefault": False,
1823
- "transport": "stdio",
1824
- "launch": {
1825
- "executable": "${nodeExecutable}",
1826
- "args": ["${serviceDir}/node_modules/@playwright/mcp/cli.js", "--browser=chromium"],
1827
- "cwd": "${serviceDir}",
1828
- "env": {},
1829
- "handshakeTimeoutSeconds": 30
1830
- },
1831
- "options": [
1832
- {
1833
- "id": "headless",
1834
- "type": "boolean",
1835
- "label": {
1836
- "es": "Navegación en segundo plano (Headless)",
1837
- "en": "Headless background mode"
1838
- },
1839
- "description": {
1840
- "es": "Desactívalo para ver la ventana del navegador durante la automatización",
1841
- "en": "Disable to display the browser window during automation"
1842
- },
1843
- "default": True,
1844
- "argsWhenTrue": ["--headless"],
1845
- "argsWhenFalse": []
1846
- }
1847
- ]
1848
- }, indent=2), encoding="utf-8")
1849
- if not pw_installer.exists():
1850
- pw_installer.write_text(json.dumps({
1851
- "schemaVersion": 1,
1852
- "type": "npm",
1853
- "product": {
1854
- "package": "@playwright/mcp",
1855
- "version": "0.0.81",
1856
- "browser": "chromium"
1857
- },
1858
- "requirements": {
1859
- "node": {"minimumMajor": 18}
1860
- }
1861
- }, indent=2), encoding="utf-8")
1862
-
1863
- # 3. memory
1864
- memory_dir = self.services_root / "memory"
1865
- memory_dir.mkdir(parents=True, exist_ok=True)
1866
- mem_service = memory_dir / "service.json"
1867
- mem_installer = memory_dir / "installer.json"
1868
- if not mem_service.exists():
1869
- mem_service.write_text(json.dumps({
1870
- "schemaVersion": 1,
1871
- "id": "memory",
1872
- "displayName": {
1873
- "es": "Memoria y Grafos (Knowledge Graph)",
1874
- "en": "Memory & Knowledge Graph"
1875
- },
1876
- "description": {
1877
- "es": "Almacenamiento persistente de entidades, preferencias y contexto histórico estructurado en un grafo de conocimiento.",
1878
- "en": "Persistent storage of entities, preferences, and historical context structured as a knowledge graph."
1879
- },
1880
- "enabledByDefault": False,
1881
- "transport": "stdio",
1882
- "launch": {
1883
- "executable": "${nodeExecutable}",
1884
- "args": ["${serviceDir}/node_modules/@modelcontextprotocol/server-memory/dist/index.js"],
1885
- "cwd": "${serviceDir}",
1886
- "env": {
1887
- "MEMORY_FILE_PATH": "${serviceDir}/memory.jsonl"
1888
- },
1889
- "handshakeTimeoutSeconds": 30
1890
- }
1891
- }, indent=2), encoding="utf-8")
1892
- if not mem_installer.exists():
1893
- mem_installer.write_text(json.dumps({
1894
- "schemaVersion": 1,
1895
- "type": "npm",
1896
- "product": {
1897
- "package": "@modelcontextprotocol/server-memory",
1898
- "version": "2026.8.31"
1899
- },
1900
- "requirements": {
1901
- "node": {"minimumMajor": 18}
1902
- }
1903
- }, indent=2), encoding="utf-8")
1904
-
1905
1684
  def _load_services(self) -> dict[str, dict]:
1906
1685
  servers = {}
1907
1686
  for directory in sorted(self.services_root.iterdir()):
@@ -1948,6 +1727,7 @@ Para instalar el MCP <nombre-del-mcp>, localiza el directorio personal del usuar
1948
1727
  "displayName": server.get("displayName", {}),
1949
1728
  "description": server.get("description", {}),
1950
1729
  "help": server.get("help"),
1730
+ "remote": server.get("remote"),
1951
1731
  "enabled": pref.get("enabled", server.get("enabledByDefault", False)),
1952
1732
  "status": "running" if running else self.states.get(server_id, "stopped"),
1953
1733
  "toolCount": len(client.tools) if running else 0,
@@ -2412,7 +2192,10 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2412
2192
  self.send_header("Content-Length", str(len(body)))
2413
2193
  self.send_cors_headers()
2414
2194
  self.end_headers()
2415
- self.wfile.write(body)
2195
+ try:
2196
+ self.wfile.write(body)
2197
+ except (BrokenPipeError, ConnectionResetError):
2198
+ self.close_connection = True
2416
2199
 
2417
2200
  def do_GET(self):
2418
2201
  t0 = time.monotonic()
@@ -2737,21 +2520,6 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2737
2520
  "content": [{"type": "text", "text": json.dumps({"success": False, "error": str(ex)}, ensure_ascii=False)}],
2738
2521
  "isError": True
2739
2522
  }
2740
- elif tool_name.startswith("mcp_"):
2741
- try:
2742
- result = GLOBAL_MCP_MANAGER.call(tool_name, tool_args)
2743
- if isinstance(result, dict) and result.get("isError"):
2744
- c_list = result.get("content", [])
2745
- if c_list and isinstance(c_list, list) and isinstance(c_list[0], dict):
2746
- tool_error_info = c_list[0].get("text", "Error en herramienta MCP")
2747
- else:
2748
- tool_error_info = "Error en herramienta MCP"
2749
- except Exception as ex:
2750
- tool_error_info = str(ex)
2751
- result = {
2752
- "content": [{"type": "text", "text": json.dumps({"success": False, "error": str(ex)}, ensure_ascii=False)}],
2753
- "isError": True
2754
- }
2755
2523
  else:
2756
2524
  error = {"code": -32601, "message": f"Herramienta local '{tool_name}' no encontrada."}
2757
2525
  elif method == "zerochat/external/status":
@@ -2793,7 +2561,11 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
2793
2561
  self.send_header("Content-Length", str(len(resp_bytes)))
2794
2562
  self.send_cors_headers()
2795
2563
  self.end_headers()
2796
- self.wfile.write(resp_bytes)
2564
+ try:
2565
+ self.wfile.write(resp_bytes)
2566
+ except (BrokenPipeError, ConnectionResetError):
2567
+ self.close_connection = True
2568
+ return
2797
2569
  self._log_res(200, f"{safe_path} {action_tag}", (time.monotonic() - t0) * 1000, error_info=error_info)
2798
2570
 
2799
2571
  def log_message(self, format, *args):
File without changes
File without changes
File without changes