@saulwade/swl-ses 1.6.1 → 1.6.5
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 +4 -4
- package/agentes/_intent-spec.md +73 -0
- package/agentes/auto-evolucion-swl.md +24 -0
- package/agentes/cloud-infra-swl.md +25 -0
- package/agentes/datos-swl.md +23 -0
- package/agentes/devops-ci-swl.md +24 -0
- package/agentes/gh-fix-ci-swl.md +275 -0
- package/agentes/migrador-swl.md +22 -0
- package/agentes/nemesis-auditor-swl.md +90 -1
- package/agentes/pagos-swl.md +25 -0
- package/agentes/release-manager-swl.md +24 -0
- package/agentes/sre-swl.md +24 -0
- package/comandos/swl/exportar-vault.md +106 -14
- package/comandos/swl/nemesis.md +70 -3
- package/comandos/swl/planear-fase.md +16 -0
- 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/aprender-de-git-diff/SKILL.md +288 -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/diseno-herramientas-agente/SKILL.md +17 -1
- package/habilidades/fastapi-experto/SKILL.md +49 -4
- package/habilidades/harness-claude-code/SKILL.md +4 -1
- package/habilidades/meta-skills-estandar/SKILL.md +6 -0
- package/habilidades/meta-skills-estandar/recursos/skill-judge-rubrica.md +281 -0
- package/habilidades/postgresql-experto/SKILL.md +80 -4
- package/habilidades/proceso-autoverificacion-evidencias/SKILL.md +258 -0
- package/habilidades/proceso-confianza-pre-implementacion/SKILL.md +246 -0
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -0
- package/habilidades/proceso-ddia-streaming/SKILL.md +231 -0
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
- package/habilidades/proceso-intent-engineering/SKILL.md +269 -0
- package/habilidades/proceso-modular-split/SKILL.md +256 -0
- package/habilidades/reducir-entropia/SKILL.md +219 -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 +115 -18
- package/hooks/lib/gateway-notify.js +70 -7
- package/hooks/lib/task-budget.js +218 -0
- package/hooks/validar-intent-spec.js +222 -0
- package/manifiestos/hooks-config.json +9 -0
- package/manifiestos/modulos.json +22 -3
- package/manifiestos/skills-lock.json +1247 -1142
- package/package.json +3 -3
- package/plugin.json +18 -2
- package/reglas/arquitectura.md +38 -0
- package/reglas/arreglar-al-detectar.md +93 -0
- package/reglas/auditorias-documentales-estructurales.md +38 -0
- package/reglas/fragmentos-compartidos.md +26 -0
- package/reglas/intent-engineering.md +214 -0
- package/reglas/registro-componentes-nuevos.md +52 -0
- package/reglas/tests-cleanup.md +220 -0
- package/schemas/agent-frontmatter.schema.json +294 -167
- package/schemas/agent-message.schema.json +73 -53
- package/schemas/agent-output-implementacion.schema.json +114 -85
- package/schemas/agent-output-planificacion.schema.json +150 -113
- package/schemas/agent-output-review.schema.json +98 -78
- package/schemas/diary-entry.schema.json +42 -10
- package/schemas/hook-profiles.schema.json +54 -39
- package/schemas/hooks-config.schema.json +89 -74
- package/schemas/instinct.schema.json +152 -115
- package/schemas/modulos.schema.json +38 -29
- package/schemas/perfiles.schema.json +36 -28
- package/schemas/plugin.schema.json +77 -64
- package/schemas/skill-evals.schema.json +119 -95
- package/schemas/skill-frontmatter.schema.json +245 -170
- package/scripts/generar-inventario.js +3 -1
- package/scripts/lib/mcp_config.py +29 -14
- package/scripts/lib/schema-version.js +164 -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/validar-manifest.js +1 -1
- package/scripts/validar.js +3 -2
- package/scripts/verificar-release.js +199 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/lib/schema-version.js
|
|
5
|
+
*
|
|
6
|
+
* Helper de compatibilidad de schemas JSON aplicando reglas backward/forward
|
|
7
|
+
* del libro "Designing Data-Intensive Applications" de Martin Kleppmann
|
|
8
|
+
* (Capítulo 4, "Encoding and Evolution", p.112).
|
|
9
|
+
*
|
|
10
|
+
* Reglas implementadas (cita textual del libro):
|
|
11
|
+
*
|
|
12
|
+
* "Backward compatibility — Newer code can read data that was written by
|
|
13
|
+
* older code.
|
|
14
|
+
*
|
|
15
|
+
* Forward compatibility — Older code can read data that was written by
|
|
16
|
+
* newer code."
|
|
17
|
+
*
|
|
18
|
+
* Aplicado a SWL: cuando un schema evoluciona, el helper indica si un
|
|
19
|
+
* documento dado puede leerse con el schema dado y bajo qué modalidad
|
|
20
|
+
* de compatibilidad.
|
|
21
|
+
*
|
|
22
|
+
* Convenciones SemVer asumidas para $schemaVersion:
|
|
23
|
+
* - MAJOR (X.0.0): breaking change (renombre, cambio de tipo, eliminación)
|
|
24
|
+
* - MINOR (1.X.0): campo nuevo opcional, enum value nuevo
|
|
25
|
+
* - PATCH (1.0.X): clarificación de descripción, sin cambio estructural
|
|
26
|
+
*
|
|
27
|
+
* Skill relacionado: proceso-ddia-fundamentos § Schema Evolution.
|
|
28
|
+
* ADR: 0026 — Versionado de schemas JSON con $schemaVersion.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parsea un string SemVer "MAJOR.MINOR.PATCH" a tupla numérica.
|
|
33
|
+
* @param {string} version
|
|
34
|
+
* @returns {[number, number, number] | null}
|
|
35
|
+
*/
|
|
36
|
+
function parsearVersion(version) {
|
|
37
|
+
if (typeof version !== 'string') return null;
|
|
38
|
+
const m = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
39
|
+
if (!m) return null;
|
|
40
|
+
return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Verifica si un documento es compatible con un schema según sus $schemaVersion.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} documento - Documento JSON con propiedad opcional $schemaVersion
|
|
47
|
+
* @param {object} schema - JSON Schema con propiedad opcional $schemaVersion
|
|
48
|
+
* @returns {{
|
|
49
|
+
* compatible: boolean,
|
|
50
|
+
* modo: "none" | "backward" | "forward" | "incompatible",
|
|
51
|
+
* advertencias: string[]
|
|
52
|
+
* }}
|
|
53
|
+
*
|
|
54
|
+
* Casos:
|
|
55
|
+
* - Ambos sin $schemaVersion → { compatible: true, modo: "none", advertencias: [warning] }
|
|
56
|
+
* - Documento sin, schema con → { compatible: true, modo: "forward", advertencias: [warning] }
|
|
57
|
+
* - Doc 1.0.0 vs Schema 1.0.0 → { compatible: true, modo: "none" }
|
|
58
|
+
* - Doc 1.0.0 vs Schema 1.1.0 → { compatible: true, modo: "forward" } (dato viejo + código nuevo)
|
|
59
|
+
* - Doc 1.1.0 vs Schema 1.0.0 → { compatible: true, modo: "backward" } (dato nuevo + código viejo)
|
|
60
|
+
* - Doc 2.0.0 vs Schema 1.0.0 → { compatible: false, modo: "incompatible" } (MAJOR rompe)
|
|
61
|
+
* - Doc 1.0.0 vs Schema 2.0.0 → { compatible: false, modo: "incompatible" }
|
|
62
|
+
* - Versión malformada → { compatible: false, modo: "incompatible", advertencias: [error] }
|
|
63
|
+
*/
|
|
64
|
+
function verificarCompatibilidad(documento, schema) {
|
|
65
|
+
const advertencias = [];
|
|
66
|
+
|
|
67
|
+
const docVerStr = documento && documento.$schemaVersion;
|
|
68
|
+
const schVerStr = schema && schema.$schemaVersion;
|
|
69
|
+
|
|
70
|
+
if (!docVerStr && !schVerStr) {
|
|
71
|
+
return {
|
|
72
|
+
compatible: true,
|
|
73
|
+
modo: 'none',
|
|
74
|
+
advertencias: [
|
|
75
|
+
'Ni documento ni schema declaran $schemaVersion. Compatibilidad asumida pero no verificable.',
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!docVerStr && schVerStr) {
|
|
81
|
+
return {
|
|
82
|
+
compatible: true,
|
|
83
|
+
modo: 'forward',
|
|
84
|
+
advertencias: [
|
|
85
|
+
`Documento sin $schemaVersion contra schema ${schVerStr}. Asumiendo forward-compat (código nuevo lee dato viejo sin versión).`,
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (docVerStr && !schVerStr) {
|
|
91
|
+
return {
|
|
92
|
+
compatible: false,
|
|
93
|
+
modo: 'incompatible',
|
|
94
|
+
advertencias: [
|
|
95
|
+
`Documento declara $schemaVersion ${docVerStr} pero schema no la declara. Schema debe actualizarse a $schemaVersion para validación correcta.`,
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const docVer = parsearVersion(docVerStr);
|
|
101
|
+
const schVer = parsearVersion(schVerStr);
|
|
102
|
+
|
|
103
|
+
if (!docVer) {
|
|
104
|
+
return {
|
|
105
|
+
compatible: false,
|
|
106
|
+
modo: 'incompatible',
|
|
107
|
+
advertencias: [`Documento $schemaVersion malformada: "${docVerStr}". Debe ser MAJOR.MINOR.PATCH (ej: "1.0.0").`],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (!schVer) {
|
|
111
|
+
return {
|
|
112
|
+
compatible: false,
|
|
113
|
+
modo: 'incompatible',
|
|
114
|
+
advertencias: [`Schema $schemaVersion malformada: "${schVerStr}". Debe ser MAJOR.MINOR.PATCH (ej: "1.0.0").`],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const [docMaj, docMin] = docVer;
|
|
119
|
+
const [schMaj, schMin] = schVer;
|
|
120
|
+
|
|
121
|
+
// MAJOR distinto → incompatible (regla DDIA Cap 4: MAJOR es breaking)
|
|
122
|
+
if (docMaj !== schMaj) {
|
|
123
|
+
return {
|
|
124
|
+
compatible: false,
|
|
125
|
+
modo: 'incompatible',
|
|
126
|
+
advertencias: [
|
|
127
|
+
`MAJOR diferente: documento ${docVerStr} vs schema ${schVerStr}. ` +
|
|
128
|
+
`Cambios MAJOR son breaking por convención SemVer. Migración explícita requerida.`,
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Mismo MAJOR.MINOR → compatible total (PATCH no afecta estructura)
|
|
134
|
+
if (docMin === schMin) {
|
|
135
|
+
return { compatible: true, modo: 'none', advertencias: [] };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Documento MINOR menor que schema → forward (código nuevo lee dato viejo)
|
|
139
|
+
if (docMin < schMin) {
|
|
140
|
+
return {
|
|
141
|
+
compatible: true,
|
|
142
|
+
modo: 'forward',
|
|
143
|
+
advertencias: [
|
|
144
|
+
`Documento ${docVerStr} más antiguo que schema ${schVerStr}. ` +
|
|
145
|
+
`Forward-compat: código nuevo debe ignorar campos nuevos ausentes en dato viejo.`,
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Documento MINOR mayor que schema → backward (código viejo lee dato nuevo)
|
|
151
|
+
return {
|
|
152
|
+
compatible: true,
|
|
153
|
+
modo: 'backward',
|
|
154
|
+
advertencias: [
|
|
155
|
+
`Documento ${docVerStr} más nuevo que schema ${schVerStr}. ` +
|
|
156
|
+
`Backward-compat: código viejo debe ignorar campos desconocidos en dato nuevo.`,
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
parsearVersion,
|
|
163
|
+
verificarCompatibilidad,
|
|
164
|
+
};
|
|
@@ -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()
|