@saulwade/swl-ses 1.6.3 → 1.6.6
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.
- package/CLAUDE.md +3 -3
- package/README.md +2 -2
- package/agentes/gh-fix-ci-swl.md +275 -0
- package/agentes/nemesis-auditor-swl.md +90 -1
- package/comandos/swl/exportar-vault.md +106 -14
- package/comandos/swl/nemesis.md +70 -3
- package/comandos/swl/release.md +62 -2
- package/comandos/swl/salud.md +32 -0
- package/comandos/swl/verificar.md +116 -2
- package/habilidades/agent-browser/SKILL.md +111 -4
- package/habilidades/agent-deep-links/SKILL.md +148 -0
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
- package/habilidades/backend-error-design/SKILL.md +221 -0
- package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
- package/habilidades/browser-research-domains/SKILL.md +635 -0
- package/habilidades/changelog-generator/SKILL.md +172 -0
- package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
- package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
- package/habilidades/fastapi-experto/SKILL.md +49 -4
- package/habilidades/harness-claude-code/SKILL.md +4 -1
- package/habilidades/postgresql-experto/SKILL.md +80 -4
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
- package/habilidades/proceso-modular-split/SKILL.md +256 -0
- package/habilidades/tdd-workflow/SKILL.md +12 -5
- package/hooks/extraccion-aprendizajes.js +8 -0
- package/hooks/lib/deep-links.js +185 -0
- package/hooks/lib/evolution-tracker.js +148 -20
- package/hooks/lib/gateway-notify.js +70 -7
- package/manifiestos/modulos.json +13 -3
- package/manifiestos/skills-lock.json +1247 -1191
- package/package.json +92 -92
- package/plugin.json +371 -362
- package/reglas/arquitectura.md +38 -0
- package/reglas/arreglar-al-detectar.md +93 -0
- package/reglas/auditorias-documentales-estructurales.md +38 -0
- package/reglas/registro-componentes-nuevos.md +14 -0
- package/reglas/tests-cleanup.md +220 -0
- package/scripts/instalador.js +72 -4
- package/scripts/lib/mcp_config.py +29 -14
- package/scripts/lib/notificaciones-telegram.js +14 -0
- package/scripts/lib/transformadores/codex.js +4 -0
- package/scripts/lib/transformadores/cursor.js +5 -0
- package/scripts/mcp-orchestrator.py +153 -131
- package/scripts/mcp-pool-manager.py +132 -107
- package/scripts/mcp-telemetry.py +139 -120
- package/scripts/verificar-release.js +199 -1
|
@@ -343,6 +343,10 @@ class TransformadorCodex extends TransformadorBase {
|
|
|
343
343
|
SWL_MCP_BASE_DIR: process.cwd(),
|
|
344
344
|
},
|
|
345
345
|
});
|
|
346
|
+
// FIX v1.6.6: confirmar al usuario que config.toml se actualizó. Antes
|
|
347
|
+
// el efecto de --with-mcp era invisible — sin lanzar Codex era imposible
|
|
348
|
+
// saber si el registro había ocurrido.
|
|
349
|
+
console.log(` + MCP server swl-memory registrado en ${configToml}`);
|
|
346
350
|
} catch (err) {
|
|
347
351
|
// No bloquear instalación si el registro del MCP falla — solo loggear
|
|
348
352
|
// El usuario verá el error y puede registrar manualmente con `codex mcp add`.
|
|
@@ -353,6 +353,11 @@ class TransformadorCursor extends TransformadorBase {
|
|
|
353
353
|
};
|
|
354
354
|
|
|
355
355
|
atomicWriteSync(rutaMcp, JSON.stringify(existente, null, 2) + '\n', 'utf8', { mode: 0o600 });
|
|
356
|
+
|
|
357
|
+
// FIX v1.6.6: confirmar al usuario que mcp.json se escribió. Antes el
|
|
358
|
+
// efecto de --with-mcp era invisible — sin abrir Cursor era imposible
|
|
359
|
+
// saber si el registro había ocurrido.
|
|
360
|
+
console.log(` + MCP server swl-memory registrado en ${rutaMcp}`);
|
|
356
361
|
}
|
|
357
362
|
}
|
|
358
363
|
|
|
@@ -31,8 +31,8 @@ from pathlib import Path
|
|
|
31
31
|
from typing import Any
|
|
32
32
|
|
|
33
33
|
# Importar helper compartido sin convertir scripts/ en paquete.
|
|
34
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent /
|
|
35
|
-
from mcp_config import cargar_config_mcp # noqa: E402
|
|
34
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent / "lib"))
|
|
35
|
+
from mcp_config import build_stdio_env, cargar_config_mcp # noqa: E402
|
|
36
36
|
|
|
37
37
|
# ---------------------------------------------------------------------------
|
|
38
38
|
# Dependencias — raw mcp SDK
|
|
@@ -41,6 +41,7 @@ try:
|
|
|
41
41
|
from mcp import ClientSession
|
|
42
42
|
from mcp.client.stdio import stdio_client, StdioServerParameters
|
|
43
43
|
from mcp.client.sse import sse_client
|
|
44
|
+
|
|
44
45
|
HAS_MCP = True
|
|
45
46
|
except ImportError:
|
|
46
47
|
HAS_MCP = False
|
|
@@ -49,8 +50,8 @@ except ImportError:
|
|
|
49
50
|
# Constantes
|
|
50
51
|
# ---------------------------------------------------------------------------
|
|
51
52
|
|
|
52
|
-
TIMEOUT_S
|
|
53
|
-
SNAPSHOT_FILE
|
|
53
|
+
TIMEOUT_S = 12
|
|
54
|
+
SNAPSHOT_FILE = Path(".planning") / "mcp-snapshot.json"
|
|
54
55
|
|
|
55
56
|
# ---------------------------------------------------------------------------
|
|
56
57
|
# Carga de config — delega a scripts/lib/mcp_config.py para deep merge
|
|
@@ -63,13 +64,9 @@ def _cargar_config(cwd: Path, config_path: str | None = None) -> dict:
|
|
|
63
64
|
return cargar_config_mcp(cwd, config_path)
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
def _build_env(cfg: dict) -> dict
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return None
|
|
70
|
-
merged = dict(os.environ)
|
|
71
|
-
merged.update(extra)
|
|
72
|
-
return merged
|
|
67
|
+
def _build_env(cfg: dict) -> dict:
|
|
68
|
+
return build_stdio_env(cfg)
|
|
69
|
+
|
|
73
70
|
|
|
74
71
|
# ---------------------------------------------------------------------------
|
|
75
72
|
# Conexion async (igual que pool-manager, duplicada para zero-imports externos)
|
|
@@ -79,24 +76,24 @@ def _build_env(cfg: dict) -> dict | None:
|
|
|
79
76
|
async def _probe_server(nombre: str, cfg: dict) -> dict:
|
|
80
77
|
"""Conecta a un servidor, devuelve su inventario completo de herramientas."""
|
|
81
78
|
resultado: dict = {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
"server": nombre,
|
|
80
|
+
"transport": "http" if "url" in cfg else "stdio",
|
|
81
|
+
"tools": [],
|
|
82
|
+
"error": None,
|
|
83
|
+
"estado": "OK",
|
|
84
|
+
"duration_ms": 0,
|
|
88
85
|
}
|
|
89
86
|
t0 = time.time()
|
|
90
87
|
try:
|
|
91
|
-
if
|
|
92
|
-
transport = sse_client(cfg[
|
|
88
|
+
if "url" in cfg:
|
|
89
|
+
transport = sse_client(cfg["url"])
|
|
93
90
|
else:
|
|
94
91
|
env = _build_env(cfg)
|
|
95
92
|
params = StdioServerParameters(
|
|
96
|
-
command=cfg[
|
|
97
|
-
args=cfg.get(
|
|
93
|
+
command=cfg["command"],
|
|
94
|
+
args=cfg.get("args", []),
|
|
98
95
|
env=env,
|
|
99
|
-
cwd=cfg.get(
|
|
96
|
+
cwd=cfg.get("cwd"),
|
|
100
97
|
)
|
|
101
98
|
transport = stdio_client(params)
|
|
102
99
|
|
|
@@ -104,21 +101,21 @@ async def _probe_server(nombre: str, cfg: dict) -> dict:
|
|
|
104
101
|
async with ClientSession(read, write) as session:
|
|
105
102
|
await asyncio.wait_for(session.initialize(), timeout=TIMEOUT_S)
|
|
106
103
|
resp = await asyncio.wait_for(session.list_tools(), timeout=TIMEOUT_S)
|
|
107
|
-
resultado[
|
|
104
|
+
resultado["tools"] = [
|
|
108
105
|
{
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
"name": t.name,
|
|
107
|
+
"description": t.description or "",
|
|
111
108
|
}
|
|
112
109
|
for t in (resp.tools or [])
|
|
113
110
|
]
|
|
114
111
|
except asyncio.TimeoutError:
|
|
115
|
-
resultado[
|
|
116
|
-
resultado[
|
|
112
|
+
resultado["error"] = f"Timeout ({TIMEOUT_S}s)"
|
|
113
|
+
resultado["estado"] = "ERROR"
|
|
117
114
|
except Exception as exc:
|
|
118
|
-
resultado[
|
|
119
|
-
resultado[
|
|
115
|
+
resultado["error"] = str(exc)
|
|
116
|
+
resultado["estado"] = "ERROR"
|
|
120
117
|
finally:
|
|
121
|
-
resultado[
|
|
118
|
+
resultado["duration_ms"] = int((time.time() - t0) * 1000)
|
|
122
119
|
return resultado
|
|
123
120
|
|
|
124
121
|
|
|
@@ -127,6 +124,7 @@ async def _probe_all(servers: dict) -> list:
|
|
|
127
124
|
tareas = [_probe_server(n, c) for n, c in servers.items()]
|
|
128
125
|
return list(await asyncio.gather(*tareas))
|
|
129
126
|
|
|
127
|
+
|
|
130
128
|
# ---------------------------------------------------------------------------
|
|
131
129
|
# Snapshot — persiste el ultimo estado conocido
|
|
132
130
|
# ---------------------------------------------------------------------------
|
|
@@ -137,21 +135,23 @@ def _guardar_snapshot(cwd: Path, resultados: list) -> None:
|
|
|
137
135
|
snap_file = cwd / SNAPSHOT_FILE
|
|
138
136
|
snap_file.parent.mkdir(parents=True, exist_ok=True)
|
|
139
137
|
snapshot = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
138
|
+
"generado": datetime.now(timezone.utc).isoformat(),
|
|
139
|
+
"servidores": resultados,
|
|
140
|
+
"resumen": {
|
|
141
|
+
"total": len(resultados),
|
|
142
|
+
"activos": sum(1 for r in resultados if r["estado"] == "OK"),
|
|
143
|
+
"errores": sum(1 for r in resultados if r["estado"] == "ERROR"),
|
|
144
|
+
"tools": sum(len(r["tools"]) for r in resultados),
|
|
147
145
|
},
|
|
148
146
|
}
|
|
149
147
|
try:
|
|
150
|
-
tmp = snap_file.with_suffix(
|
|
151
|
-
tmp.write_text(
|
|
148
|
+
tmp = snap_file.with_suffix(".tmp")
|
|
149
|
+
tmp.write_text(
|
|
150
|
+
json.dumps(snapshot, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
151
|
+
)
|
|
152
152
|
os.replace(tmp, snap_file)
|
|
153
153
|
except Exception as exc:
|
|
154
|
-
sys.stderr.write(f
|
|
154
|
+
sys.stderr.write(f"[mcp-orchestrator] No se pudo guardar snapshot: {exc}\n")
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
def _cargar_snapshot(cwd: Path) -> dict | None:
|
|
@@ -159,10 +159,11 @@ def _cargar_snapshot(cwd: Path) -> dict | None:
|
|
|
159
159
|
if not snap_file.exists():
|
|
160
160
|
return None
|
|
161
161
|
try:
|
|
162
|
-
return json.loads(snap_file.read_text(encoding=
|
|
162
|
+
return json.loads(snap_file.read_text(encoding="utf-8"))
|
|
163
163
|
except Exception:
|
|
164
164
|
return None
|
|
165
165
|
|
|
166
|
+
|
|
166
167
|
# ---------------------------------------------------------------------------
|
|
167
168
|
# Telemetria opcional (importa mcp-telemetry si esta disponible)
|
|
168
169
|
# ---------------------------------------------------------------------------
|
|
@@ -171,16 +172,18 @@ def _cargar_snapshot(cwd: Path) -> dict | None:
|
|
|
171
172
|
def _registrar_traza_discovery(cwd: Path, resumen: dict) -> None:
|
|
172
173
|
"""Registra una traza del discovery si mcp-telemetry esta disponible."""
|
|
173
174
|
try:
|
|
174
|
-
sys.path.insert(0, str(cwd /
|
|
175
|
+
sys.path.insert(0, str(cwd / "scripts"))
|
|
175
176
|
from mcp_telemetry import registrar_traza # type: ignore
|
|
177
|
+
|
|
176
178
|
registrar_traza(
|
|
177
|
-
cwd
|
|
178
|
-
nombre
|
|
179
|
-
atributos
|
|
180
|
-
estado
|
|
179
|
+
cwd=cwd,
|
|
180
|
+
nombre="mcp:discovery",
|
|
181
|
+
atributos=resumen,
|
|
182
|
+
estado="OK" if resumen.get("errores", 0) == 0 else "ERROR",
|
|
181
183
|
)
|
|
182
184
|
except ImportError:
|
|
183
|
-
pass
|
|
185
|
+
pass # mcp-telemetry no disponible — silencioso
|
|
186
|
+
|
|
184
187
|
|
|
185
188
|
# ---------------------------------------------------------------------------
|
|
186
189
|
# Subcomandos
|
|
@@ -188,87 +191,100 @@ def _registrar_traza_discovery(cwd: Path, resumen: dict) -> None:
|
|
|
188
191
|
|
|
189
192
|
|
|
190
193
|
async def cmd_status(servers: dict, cwd: Path, as_json: bool, con_traza: bool) -> None:
|
|
191
|
-
out
|
|
194
|
+
out = sys.stdout.write
|
|
192
195
|
resultados = await _probe_all(servers)
|
|
193
196
|
_guardar_snapshot(cwd, resultados)
|
|
194
197
|
|
|
195
198
|
resumen = {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
"total": len(resultados),
|
|
200
|
+
"activos": sum(1 for r in resultados if r["estado"] == "OK"),
|
|
201
|
+
"errores": sum(1 for r in resultados if r["estado"] == "ERROR"),
|
|
202
|
+
"tools": sum(len(r["tools"]) for r in resultados),
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
if con_traza:
|
|
203
206
|
_registrar_traza_discovery(cwd, resumen)
|
|
204
207
|
|
|
205
208
|
if as_json:
|
|
206
|
-
out(
|
|
207
|
-
|
|
209
|
+
out(
|
|
210
|
+
json.dumps(
|
|
211
|
+
{"resumen": resumen, "servidores": resultados},
|
|
212
|
+
ensure_ascii=False,
|
|
213
|
+
indent=2,
|
|
214
|
+
)
|
|
215
|
+
+ "\n"
|
|
216
|
+
)
|
|
208
217
|
return
|
|
209
218
|
|
|
210
|
-
out(
|
|
211
|
-
out(
|
|
219
|
+
out("\nEstado de servidores MCP:\n")
|
|
220
|
+
out("-" * 68 + "\n")
|
|
212
221
|
for r in resultados:
|
|
213
|
-
estado = r[
|
|
214
|
-
tools
|
|
215
|
-
ms
|
|
216
|
-
trans
|
|
217
|
-
err
|
|
218
|
-
out(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
estado = r["estado"]
|
|
223
|
+
tools = len(r["tools"])
|
|
224
|
+
ms = r["duration_ms"]
|
|
225
|
+
trans = r["transport"]
|
|
226
|
+
err = f' -- {r["error"]}' if r["error"] else ""
|
|
227
|
+
out(
|
|
228
|
+
f' {r["server"]:<28} {estado:<5} {tools:>3} tools {ms:>5}ms [{trans}]{err}\n'
|
|
229
|
+
)
|
|
230
|
+
out("\n")
|
|
231
|
+
out(
|
|
232
|
+
f' Total: {resumen["total"]} servidores, {resumen["activos"]} activos, '
|
|
233
|
+
f'{resumen["tools"]} herramientas disponibles\n'
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def cmd_discover(
|
|
238
|
+
servers: dict, cwd: Path, as_json: bool, con_traza: bool
|
|
239
|
+
) -> None:
|
|
240
|
+
out = sys.stdout.write
|
|
226
241
|
resultados = await _probe_all(servers)
|
|
227
242
|
_guardar_snapshot(cwd, resultados)
|
|
228
243
|
|
|
229
244
|
if con_traza:
|
|
230
245
|
resumen = {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
"total": len(resultados),
|
|
247
|
+
"activos": sum(1 for r in resultados if r["estado"] == "OK"),
|
|
248
|
+
"errores": sum(1 for r in resultados if r["estado"] == "ERROR"),
|
|
249
|
+
"tools": sum(len(r["tools"]) for r in resultados),
|
|
235
250
|
}
|
|
236
251
|
_registrar_traza_discovery(cwd, resumen)
|
|
237
252
|
|
|
238
253
|
if as_json:
|
|
239
|
-
out(json.dumps(resultados, ensure_ascii=False, indent=2) +
|
|
254
|
+
out(json.dumps(resultados, ensure_ascii=False, indent=2) + "\n")
|
|
240
255
|
return
|
|
241
256
|
|
|
242
257
|
for r in resultados:
|
|
243
|
-
if r[
|
|
258
|
+
if r["error"]:
|
|
244
259
|
out(f'\n[{r["server"]}] ERROR: {r["error"]}\n')
|
|
245
260
|
continue
|
|
246
261
|
out(f'\n[{r["server"]}] {len(r["tools"])} herramientas:\n')
|
|
247
|
-
for t in r[
|
|
248
|
-
desc = t[
|
|
262
|
+
for t in r["tools"]:
|
|
263
|
+
desc = t["description"]
|
|
249
264
|
if len(desc) > 72:
|
|
250
|
-
desc = desc[:69] +
|
|
265
|
+
desc = desc[:69] + "..."
|
|
251
266
|
out(f' - {t["name"]:<32} {desc}\n')
|
|
252
267
|
|
|
253
268
|
|
|
254
269
|
async def cmd_find_tool(servers: dict, keyword: str, as_json: bool) -> None:
|
|
255
|
-
out
|
|
270
|
+
out = sys.stdout.write
|
|
256
271
|
resultados = await _probe_all(servers)
|
|
257
|
-
kw_lower
|
|
272
|
+
kw_lower = keyword.lower()
|
|
258
273
|
|
|
259
274
|
encontradas: list = []
|
|
260
275
|
for r in resultados:
|
|
261
|
-
for t in r.get(
|
|
262
|
-
if
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
276
|
+
for t in r.get("tools", []):
|
|
277
|
+
if kw_lower in t["name"].lower() or kw_lower in t["description"].lower():
|
|
278
|
+
encontradas.append(
|
|
279
|
+
{
|
|
280
|
+
"server": r["server"],
|
|
281
|
+
"name": t["name"],
|
|
282
|
+
"description": t["description"],
|
|
283
|
+
}
|
|
284
|
+
)
|
|
269
285
|
|
|
270
286
|
if as_json:
|
|
271
|
-
out(json.dumps(encontradas, ensure_ascii=False, indent=2) +
|
|
287
|
+
out(json.dumps(encontradas, ensure_ascii=False, indent=2) + "\n")
|
|
272
288
|
return
|
|
273
289
|
|
|
274
290
|
if not encontradas:
|
|
@@ -276,101 +292,107 @@ async def cmd_find_tool(servers: dict, keyword: str, as_json: bool) -> None:
|
|
|
276
292
|
return
|
|
277
293
|
|
|
278
294
|
out(f'\nHerramientas que coinciden con "{keyword}" ({len(encontradas)}):\n')
|
|
279
|
-
out(
|
|
295
|
+
out("-" * 68 + "\n")
|
|
280
296
|
for e in encontradas:
|
|
281
|
-
desc = e[
|
|
297
|
+
desc = e["description"]
|
|
282
298
|
if len(desc) > 60:
|
|
283
|
-
desc = desc[:57] +
|
|
299
|
+
desc = desc[:57] + "..."
|
|
284
300
|
out(f' [{e["server"]}] {e["name"]:<28} {desc}\n')
|
|
285
301
|
|
|
286
302
|
|
|
287
303
|
def cmd_summary(cwd: Path, as_json: bool) -> None:
|
|
288
304
|
"""Muestra el ultimo snapshot guardado sin re-conectar a los servidores."""
|
|
289
|
-
out
|
|
305
|
+
out = sys.stdout.write
|
|
290
306
|
snapshot = _cargar_snapshot(cwd)
|
|
291
307
|
|
|
292
308
|
if not snapshot:
|
|
293
|
-
out(
|
|
309
|
+
out("Sin snapshot disponible. Ejecuta primero: mcp-orchestrator.py status\n")
|
|
294
310
|
return
|
|
295
311
|
|
|
296
312
|
if as_json:
|
|
297
|
-
out(json.dumps(snapshot, ensure_ascii=False, indent=2) +
|
|
313
|
+
out(json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n")
|
|
298
314
|
return
|
|
299
315
|
|
|
300
|
-
generado = snapshot.get(
|
|
301
|
-
resumen
|
|
302
|
-
out(f
|
|
303
|
-
out(
|
|
304
|
-
f'
|
|
316
|
+
generado = snapshot.get("generado", "?")[:19]
|
|
317
|
+
resumen = snapshot.get("resumen", {})
|
|
318
|
+
out(f"\nResumen MCP (snapshot del {generado} UTC):\n")
|
|
319
|
+
out(
|
|
320
|
+
f' Servidores: {resumen.get("total", 0)} '
|
|
321
|
+
f'({resumen.get("activos", 0)} activos, {resumen.get("errores", 0)} con error)\n'
|
|
322
|
+
)
|
|
305
323
|
out(f' Herramientas disponibles: {resumen.get("tools", 0)}\n')
|
|
306
|
-
out(
|
|
307
|
-
for r in snapshot.get(
|
|
308
|
-
estado = r.get(
|
|
309
|
-
tools
|
|
310
|
-
ms
|
|
324
|
+
out("\nDetalle por servidor:\n")
|
|
325
|
+
for r in snapshot.get("servidores", []):
|
|
326
|
+
estado = r.get("estado", "?")
|
|
327
|
+
tools = len(r.get("tools", []))
|
|
328
|
+
ms = r.get("duration_ms", 0)
|
|
311
329
|
out(f' {r["server"]:<28} {estado:<5} {tools:>3} tools {ms:>5}ms\n')
|
|
312
330
|
|
|
331
|
+
|
|
313
332
|
# ---------------------------------------------------------------------------
|
|
314
333
|
# main()
|
|
315
334
|
# ---------------------------------------------------------------------------
|
|
316
335
|
|
|
317
336
|
|
|
318
337
|
def main() -> None:
|
|
319
|
-
if hasattr(sys.stdout,
|
|
320
|
-
sys.stdout.reconfigure(encoding=
|
|
338
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
339
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
321
340
|
|
|
322
341
|
if not HAS_MCP:
|
|
323
342
|
sys.stderr.write(
|
|
324
|
-
|
|
325
|
-
|
|
343
|
+
"[mcp-orchestrator] ERROR: la libreria mcp no esta instalada.\n"
|
|
344
|
+
"[mcp-orchestrator] Ejecuta: pip install mcp\n"
|
|
326
345
|
)
|
|
327
346
|
sys.exit(1)
|
|
328
347
|
|
|
329
348
|
parser = argparse.ArgumentParser(
|
|
330
|
-
description=
|
|
349
|
+
description="Orquestador multi-servidor MCP para swl-ses"
|
|
331
350
|
)
|
|
332
|
-
parser.add_argument(
|
|
333
|
-
parser.add_argument(
|
|
334
|
-
parser.add_argument(
|
|
335
|
-
|
|
336
|
-
|
|
351
|
+
parser.add_argument("--json", action="store_true", help="Salida en formato JSON")
|
|
352
|
+
parser.add_argument("--cwd", default=".", help="Directorio raiz del proyecto")
|
|
353
|
+
parser.add_argument(
|
|
354
|
+
"--trace", action="store_true", help="Registrar resultado en .planning/traces/"
|
|
355
|
+
)
|
|
356
|
+
parser.add_argument("--config", default=None, help="Ruta al archivo de config MCP")
|
|
337
357
|
|
|
338
|
-
sub = parser.add_subparsers(dest=
|
|
358
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
339
359
|
|
|
340
|
-
sub.add_parser(
|
|
341
|
-
sub.add_parser(
|
|
360
|
+
sub.add_parser("status", help="Health check de todos los servidores MCP")
|
|
361
|
+
sub.add_parser("discover", help="Lista completa de herramientas por servidor")
|
|
342
362
|
|
|
343
|
-
p_find = sub.add_parser(
|
|
344
|
-
|
|
363
|
+
p_find = sub.add_parser(
|
|
364
|
+
"find-tool", help="Busca herramienta por nombre o descripcion"
|
|
365
|
+
)
|
|
366
|
+
p_find.add_argument("keyword", help="Texto a buscar en nombre o descripcion")
|
|
345
367
|
|
|
346
|
-
sub.add_parser(
|
|
368
|
+
sub.add_parser("summary", help="Muestra el ultimo snapshot sin reconectar")
|
|
347
369
|
|
|
348
|
-
args
|
|
349
|
-
cwd
|
|
350
|
-
as_json = bool(getattr(args,
|
|
351
|
-
traza
|
|
370
|
+
args = parser.parse_args()
|
|
371
|
+
cwd = Path(args.cwd).resolve()
|
|
372
|
+
as_json = bool(getattr(args, "json", False))
|
|
373
|
+
traza = bool(getattr(args, "trace", False))
|
|
352
374
|
|
|
353
|
-
if args.cmd ==
|
|
375
|
+
if args.cmd == "summary":
|
|
354
376
|
cmd_summary(cwd, as_json)
|
|
355
377
|
return
|
|
356
378
|
|
|
357
379
|
servers = _cargar_config(cwd, args.config)
|
|
358
380
|
if not servers:
|
|
359
381
|
sys.stderr.write(
|
|
360
|
-
|
|
382
|
+
"[mcp-orchestrator] No se encontraron servidores MCP.\n"
|
|
361
383
|
'[mcp-orchestrator] Verifica la clave "mcpServers" en .claude/settings.json\n'
|
|
362
384
|
)
|
|
363
385
|
sys.exit(1)
|
|
364
386
|
|
|
365
|
-
if args.cmd ==
|
|
387
|
+
if args.cmd == "status":
|
|
366
388
|
asyncio.run(cmd_status(servers, cwd, as_json, traza))
|
|
367
|
-
elif args.cmd ==
|
|
389
|
+
elif args.cmd == "discover":
|
|
368
390
|
asyncio.run(cmd_discover(servers, cwd, as_json, traza))
|
|
369
|
-
elif args.cmd ==
|
|
391
|
+
elif args.cmd == "find-tool":
|
|
370
392
|
asyncio.run(cmd_find_tool(servers, args.keyword, as_json))
|
|
371
393
|
else:
|
|
372
394
|
parser.print_help()
|
|
373
395
|
|
|
374
396
|
|
|
375
|
-
if __name__ ==
|
|
397
|
+
if __name__ == "__main__":
|
|
376
398
|
main()
|