Primer commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
"""MCP server stdio para MP Manager.
|
||||
|
||||
Expone herramientas tipadas para que Claude Code opere el ecosistema
|
||||
(audits, syncs, búsquedas, scripts) sin pasar por el SPA ni por Bash crudo.
|
||||
|
||||
Entry point: `python -m mcp_server`
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Entry point: `python -m mcp_server` arranca el servidor MCP por stdio."""
|
||||
|
||||
from .server import build_server
|
||||
|
||||
|
||||
def main():
|
||||
server = build_server()
|
||||
# FastMCP.run() expone stdio por defecto, que es lo que Claude Code consume.
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Adapters: invocan funciones internas del proyecto desde el MCP server.
|
||||
|
||||
Filosofía: las tools del MCP llaman funciones Python directamente (no HTTP).
|
||||
Para scripts que no exportan función pública, usan subprocess con --json y
|
||||
parsean stdout. Los reportes grandes se vuelcan a `generated/agent/runs/`
|
||||
y la tool devuelve solo el path + summary para no inflar el contexto LLM.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
# Asegurar que la raíz del repo esté en el PYTHONPATH cuando se ejecuta
|
||||
# `python -m mcp_server` desde el cwd del proyecto.
|
||||
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _ROOT not in sys.path:
|
||||
sys.path.insert(0, _ROOT)
|
||||
|
||||
import paths # noqa: E402
|
||||
|
||||
SCRIPTS_DIR = os.path.join(_ROOT, "scripts")
|
||||
PYTHON_EXE = sys.executable
|
||||
LARGE_PAYLOAD_THRESHOLD = 8000 # chars; sobre esto, volcamos a archivo
|
||||
|
||||
|
||||
def new_run_id() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def _agent_run_path(tool_name: str, ext: str = "json") -> str:
|
||||
os.makedirs(paths.AGENT_RUNS_DIR, exist_ok=True)
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||
return os.path.join(paths.AGENT_RUNS_DIR, f"{tool_name}_{ts}.{ext}")
|
||||
|
||||
|
||||
def maybe_offload(tool_name: str, payload: Any) -> dict:
|
||||
"""Si el payload es grande, lo escribe a disco y devuelve {path, summary}.
|
||||
|
||||
Devuelve siempre un dict con la forma {ok, summary, details?, report_path?}.
|
||||
"""
|
||||
try:
|
||||
serialized = json.dumps(payload, ensure_ascii=False)
|
||||
except (TypeError, ValueError):
|
||||
serialized = str(payload)
|
||||
if len(serialized) > LARGE_PAYLOAD_THRESHOLD:
|
||||
path = _agent_run_path(tool_name)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(serialized)
|
||||
summary = _summarize(payload)
|
||||
return {"ok": True, "summary": summary, "report_path": path}
|
||||
return {"ok": True, "summary": _summarize(payload), "details": payload}
|
||||
|
||||
|
||||
def _summarize(payload: Any) -> dict:
|
||||
"""Genera un summary compacto: claves top-level, conteos, primeras filas."""
|
||||
if isinstance(payload, dict):
|
||||
out = {"type": "dict", "keys": list(payload.keys())[:20]}
|
||||
if "summary" in payload:
|
||||
out["summary"] = payload["summary"]
|
||||
for k, v in payload.items():
|
||||
if isinstance(v, list):
|
||||
out[f"{k}_count"] = len(v)
|
||||
return out
|
||||
if isinstance(payload, list):
|
||||
return {"type": "list", "count": len(payload), "first": payload[:3]}
|
||||
return {"type": type(payload).__name__, "value": str(payload)[:200]}
|
||||
|
||||
|
||||
def run_python_script(
|
||||
name: str,
|
||||
args: list[str] | None = None,
|
||||
timeout: int = 600,
|
||||
expect_json: bool = False,
|
||||
) -> dict:
|
||||
"""Lanza un script de `scripts/` por subprocess.
|
||||
|
||||
Devuelve {ok, exit_code, stdout, stderr, parsed?}. Si expect_json=True y el
|
||||
script imprimió JSON, lo parsea en `parsed` y aplica maybe_offload sobre él.
|
||||
"""
|
||||
if not name.endswith(".py"):
|
||||
name = name + ".py"
|
||||
script_path = os.path.join(SCRIPTS_DIR, name)
|
||||
if not os.path.isfile(script_path):
|
||||
return {"ok": False, "error": f"script not found: {name}"}
|
||||
cmd = [PYTHON_EXE, script_path] + (args or [])
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=timeout,
|
||||
cwd=_ROOT,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"ok": False, "error": "timeout", "timeout_sec": timeout}
|
||||
|
||||
out: dict[str, Any] = {
|
||||
"ok": proc.returncode == 0,
|
||||
"exit_code": proc.returncode,
|
||||
"command": " ".join(args or []),
|
||||
}
|
||||
if expect_json and proc.stdout.strip():
|
||||
try:
|
||||
parsed = json.loads(proc.stdout)
|
||||
offload = maybe_offload(name[:-3], parsed)
|
||||
out.update(offload)
|
||||
out["stderr_tail"] = proc.stderr[-500:] if proc.stderr else ""
|
||||
return out
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
out["stdout"] = proc.stdout[-4000:]
|
||||
out["stderr"] = proc.stderr[-1000:]
|
||||
return out
|
||||
|
||||
|
||||
# --- Adapters específicos (funciones Python directas) ---
|
||||
|
||||
|
||||
def list_accounts_adapter() -> list[dict]:
|
||||
"""Devuelve cuentas conocidas desde la DB local."""
|
||||
import db
|
||||
return db.get_accounts() or []
|
||||
|
||||
|
||||
def get_account_adapter(location_id: str) -> dict | None:
|
||||
import db
|
||||
return db.get_account(location_id)
|
||||
|
||||
|
||||
def search_contacts_adapter(
|
||||
location_id: str,
|
||||
query: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
without_opp: bool = False,
|
||||
) -> list[dict]:
|
||||
import db
|
||||
return db.get_contacts(location_id, search_query=query, limit=limit, offset=offset, without_opp=without_opp)
|
||||
|
||||
|
||||
def get_contact_adapter(location_id: str, contact_id: str) -> dict | None:
|
||||
import db
|
||||
return db.get_contact_by_id(location_id, contact_id)
|
||||
|
||||
|
||||
def get_opportunities_adapter(location_id: str, pipeline_id: str | None = None) -> list[dict]:
|
||||
import db
|
||||
return db.get_opportunities(location_id, pipeline_id=pipeline_id)
|
||||
|
||||
|
||||
def get_pipelines_adapter(location_id: str) -> list[dict]:
|
||||
import db
|
||||
return db.get_pipelines(location_id)
|
||||
|
||||
|
||||
def get_account_metrics_adapter(location_id: str) -> dict:
|
||||
import db
|
||||
return db.get_account_metrics(location_id)
|
||||
|
||||
|
||||
def get_global_metrics_adapter() -> dict:
|
||||
import db
|
||||
return db.get_global_metrics()
|
||||
|
||||
|
||||
def get_workflows_adapter(location_id: str | None = None) -> list[dict]:
|
||||
import db
|
||||
return db.get_workflows(location_id=location_id)
|
||||
|
||||
|
||||
def get_sync_logs_adapter(limit: int = 20) -> list[dict]:
|
||||
import db
|
||||
return db.get_sync_logs(limit=limit)
|
||||
|
||||
|
||||
def get_error_logs_adapter(limit: int = 50) -> list[dict]:
|
||||
import db
|
||||
return db.get_error_logs(limit=limit)
|
||||
|
||||
|
||||
def sync_missing_contacts_adapter(
|
||||
contact_ids: list[str] | None = None,
|
||||
dry_run: bool = True,
|
||||
run_id: str | None = None,
|
||||
) -> dict:
|
||||
"""Sincroniza contactos faltantes de sucursal → Marca.
|
||||
|
||||
Defaults a dry_run=True. Si dry_run=False, requiere run_id (lo genera si no
|
||||
se pasa) y queda registrado en script_audit para rollback.
|
||||
"""
|
||||
from scripts import sync_missing_contacts_to_brand as mod
|
||||
if not dry_run and not run_id:
|
||||
run_id = new_run_id()
|
||||
result = mod.run_sync(contact_ids=contact_ids, dry_run=dry_run, run_id=run_id)
|
||||
return {"run_id": run_id, "dry_run": dry_run, "result": result}
|
||||
|
||||
|
||||
def sync_missing_opps_adapter(
|
||||
opp_ids: list[str] | None = None,
|
||||
dry_run: bool = True,
|
||||
run_id: str | None = None,
|
||||
) -> dict:
|
||||
"""Sincroniza oportunidades faltantes de sucursal → Marca."""
|
||||
from scripts import sync_missing_opps_to_brand as mod
|
||||
if not dry_run and not run_id:
|
||||
run_id = new_run_id()
|
||||
result = mod.run_sync(opp_ids=opp_ids, dry_run=dry_run, run_id=run_id)
|
||||
return {"run_id": run_id, "dry_run": dry_run, "result": result}
|
||||
|
||||
|
||||
# --- Catálogo de scripts disponibles ---
|
||||
|
||||
|
||||
def script_catalog() -> dict:
|
||||
"""Devuelve el inventario completo de scripts con su estado de auditoría."""
|
||||
if not os.path.exists(paths.AGENT_AUDIT_REPORT):
|
||||
return {"error": "audit_report.json no existe — corre scripts/audit_agent_readiness.py"}
|
||||
with open(paths.AGENT_AUDIT_REPORT, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Genera `generated/agent/tools_manifest.json`.
|
||||
|
||||
Fuente única de verdad navegable para LLM/humanos: lista de tools MCP,
|
||||
scripts disponibles, endpoints FastAPI y su estado de cumplimiento.
|
||||
|
||||
Se ejecuta automáticamente al arrancar el server. También puede invocarse
|
||||
manual: python -m mcp_server.manifest
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _ROOT not in sys.path:
|
||||
sys.path.insert(0, _ROOT)
|
||||
|
||||
import paths # noqa: E402
|
||||
|
||||
# Catálogo declarativo de tools MCP (mantener sincronizado con server.py).
|
||||
TOOLS = [
|
||||
{"name": "list_accounts", "category": "accounts", "mutates": False, "desc": "Lista cuentas Marca+sucursales."},
|
||||
{"name": "get_account", "category": "accounts", "mutates": False, "desc": "Detalle de una cuenta."},
|
||||
{"name": "get_global_metrics", "category": "metrics", "mutates": False, "desc": "Métricas globales."},
|
||||
{"name": "get_account_metrics", "category": "metrics", "mutates": False, "desc": "Métricas por sucursal."},
|
||||
{"name": "search_contacts", "category": "contacts", "mutates": False, "desc": "Busca contactos en cache local."},
|
||||
{"name": "get_contact", "category": "contacts", "mutates": False, "desc": "Detalle de contacto."},
|
||||
{"name": "get_opportunities", "category": "opps", "mutates": False, "desc": "Oportunidades por location."},
|
||||
{"name": "get_pipelines", "category": "opps", "mutates": False, "desc": "Pipelines/etapas por location."},
|
||||
{"name": "get_workflows", "category": "workflows", "mutates": False, "desc": "Workflows por location."},
|
||||
{"name": "sync_missing_contacts", "category": "sync", "mutates": True, "desc": "Sucursal→Marca contactos faltantes (dry-run default)."},
|
||||
{"name": "sync_missing_opps", "category": "sync", "mutates": True, "desc": "Sucursal→Marca opps faltantes (dry-run default)."},
|
||||
{"name": "sync_logs", "category": "ops", "mutates": False, "desc": "Logs de sincronización."},
|
||||
{"name": "error_logs", "category": "ops", "mutates": False, "desc": "Errores recientes."},
|
||||
{"name": "agent_audit_report", "category": "ops", "mutates": False, "desc": "Reporte de salud agentica."},
|
||||
{"name": "script_catalog", "category": "ops", "mutates": False, "desc": "Manifest navegable (este archivo)."},
|
||||
{"name": "run_script", "category": "advanced", "mutates": True, "desc": "Ejecuta script arbitrario de scripts/."},
|
||||
]
|
||||
|
||||
|
||||
def _load_audit_report() -> dict | None:
|
||||
if not os.path.exists(paths.AGENT_AUDIT_REPORT):
|
||||
return None
|
||||
try:
|
||||
with open(paths.AGENT_AUDIT_REPORT, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
|
||||
def build_manifest() -> dict:
|
||||
audit = _load_audit_report()
|
||||
scripts: list[dict] = []
|
||||
endpoints: list[dict] = []
|
||||
if audit:
|
||||
for s in audit.get("scripts", []):
|
||||
scripts.append({
|
||||
"name": s["name"],
|
||||
"category": s["category"],
|
||||
"is_mutator": s["is_mutator"],
|
||||
"registered_in_metadata": s["registered_in_metadata"],
|
||||
"has_json": s["has_json_flag"],
|
||||
"has_apply": s["has_apply_flag"],
|
||||
"has_run_id": s["has_run_id_flag"],
|
||||
"issues": s["issues"],
|
||||
"suggestion": s["suggestion"],
|
||||
"docstring": s["docstring"],
|
||||
})
|
||||
endpoints = audit.get("endpoints", [])
|
||||
return {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"tools": TOOLS,
|
||||
"scripts": scripts,
|
||||
"endpoints": endpoints,
|
||||
"notes": {
|
||||
"apply_confirm_token": "I-HAVE-USER-CONFIRMATION",
|
||||
"dry_run_default": True,
|
||||
"audit_source": "generated/agent/audit_report.json",
|
||||
"refresh_audit": "python scripts/audit_agent_readiness.py",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def write_manifest() -> str:
|
||||
os.makedirs(paths.AGENT_DIR, exist_ok=True)
|
||||
manifest = build_manifest()
|
||||
with open(paths.AGENT_MANIFEST_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(manifest, f, indent=2, ensure_ascii=False)
|
||||
return paths.AGENT_MANIFEST_PATH
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
path = write_manifest()
|
||||
print(f"manifest written: {path}")
|
||||
@@ -0,0 +1,225 @@
|
||||
"""Construcción del servidor FastMCP y registro de tools.
|
||||
|
||||
Tools expuestas (ver docs/AGENT_TOOLS.md para el catálogo completo):
|
||||
|
||||
Cuentas / metadatos:
|
||||
list_accounts, get_account, get_global_metrics, get_account_metrics
|
||||
|
||||
Datos cacheados (SQLite local — refresca con sync_all_accounts):
|
||||
search_contacts, get_contact, get_opportunities, get_pipelines, get_workflows
|
||||
|
||||
Operaciones (con dry_run por defecto, requieren confirm_token para aplicar):
|
||||
sync_missing_contacts, sync_missing_opps
|
||||
|
||||
Operacional / observabilidad:
|
||||
sync_logs, error_logs, agent_audit_report, script_catalog
|
||||
|
||||
Genéricas (cola larga — uso avanzado):
|
||||
run_script(name, args, expect_json)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from . import adapters
|
||||
from .manifest import write_manifest
|
||||
|
||||
# Token literal que el LLM debe pasar como `confirm_token` para aplicar
|
||||
# cambios reales. Documentado en docs/AGENT_TOOLS.md y en la descripción
|
||||
# de cada tool mutadora. La idea es que el LLM lo solicite explícitamente
|
||||
# al usuario antes de invocar con apply=True.
|
||||
APPLY_CONFIRM_TOKEN = "I-HAVE-USER-CONFIRMATION"
|
||||
|
||||
|
||||
def _require_confirm(apply: bool, confirm_token: str | None) -> str | None:
|
||||
if not apply:
|
||||
return None
|
||||
if confirm_token != APPLY_CONFIRM_TOKEN:
|
||||
return (
|
||||
f"apply=True requiere confirm_token='{APPLY_CONFIRM_TOKEN}'. "
|
||||
"Pide confirmación al usuario antes de pasarlo. Default es dry-run."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def build_server() -> FastMCP:
|
||||
mcp = FastMCP("mp-manager")
|
||||
|
||||
# Regenerar manifest al arrancar para que siempre refleje el estado actual.
|
||||
try:
|
||||
write_manifest()
|
||||
except Exception:
|
||||
# No bloqueamos el arranque si el manifest falla; el server sigue siendo útil.
|
||||
pass
|
||||
|
||||
# ---------- Cuentas / metadatos ----------
|
||||
|
||||
@mcp.tool(description="Lista todas las cuentas (Marca + 49 sucursales) desde SQLite local. Read-only.")
|
||||
def list_accounts() -> dict:
|
||||
accounts = adapters.list_accounts_adapter()
|
||||
return adapters.maybe_offload("list_accounts", accounts)
|
||||
|
||||
@mcp.tool(description="Detalle de una cuenta por location_id desde SQLite local.")
|
||||
def get_account(location_id: str) -> dict:
|
||||
acc = adapters.get_account_adapter(location_id)
|
||||
return {"ok": acc is not None, "account": acc}
|
||||
|
||||
@mcp.tool(description="Métricas globales: total contactos, opps, sucursales activas. Read-only desde SQLite.")
|
||||
def get_global_metrics() -> dict:
|
||||
return {"ok": True, "metrics": adapters.get_global_metrics_adapter()}
|
||||
|
||||
@mcp.tool(description="Métricas de una sucursal (contactos, opps por status, pipelines). Read-only desde SQLite.")
|
||||
def get_account_metrics(location_id: str) -> dict:
|
||||
return {"ok": True, "metrics": adapters.get_account_metrics_adapter(location_id)}
|
||||
|
||||
# ---------- Datos cacheados ----------
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Busca contactos en una location (cache SQLite). "
|
||||
"query matchea nombre/email/teléfono. "
|
||||
"without_opp=True filtra contactos sin oportunidad."
|
||||
)
|
||||
)
|
||||
def search_contacts(
|
||||
location_id: str,
|
||||
query: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
without_opp: bool = False,
|
||||
) -> dict:
|
||||
rows = adapters.search_contacts_adapter(
|
||||
location_id, query=query, limit=limit, offset=offset, without_opp=without_opp
|
||||
)
|
||||
return adapters.maybe_offload("search_contacts", rows)
|
||||
|
||||
@mcp.tool(description="Detalle completo de un contacto por (location_id, contact_id) desde SQLite.")
|
||||
def get_contact(location_id: str, contact_id: str) -> dict:
|
||||
c = adapters.get_contact_adapter(location_id, contact_id)
|
||||
return {"ok": c is not None, "contact": c}
|
||||
|
||||
@mcp.tool(description="Oportunidades de una location (opcionalmente filtra por pipeline_id). Cache SQLite.")
|
||||
def get_opportunities(location_id: str, pipeline_id: str | None = None) -> dict:
|
||||
rows = adapters.get_opportunities_adapter(location_id, pipeline_id=pipeline_id)
|
||||
return adapters.maybe_offload("get_opportunities", rows)
|
||||
|
||||
@mcp.tool(description="Pipelines y etapas de una location. Cache SQLite.")
|
||||
def get_pipelines(location_id: str) -> dict:
|
||||
rows = adapters.get_pipelines_adapter(location_id)
|
||||
return {"ok": True, "pipelines": rows}
|
||||
|
||||
@mcp.tool(description="Workflows de una location (o de todas si se omite location_id). Cache SQLite.")
|
||||
def get_workflows(location_id: str | None = None) -> dict:
|
||||
rows = adapters.get_workflows_adapter(location_id=location_id)
|
||||
return adapters.maybe_offload("get_workflows", rows)
|
||||
|
||||
# ---------- Operaciones (mutadoras con guard) ----------
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Sincroniza contactos faltantes de sucursales hacia Marca. "
|
||||
"Default dry_run=True (planifica, no aplica). "
|
||||
"Para apply=True debes pasar confirm_token='I-HAVE-USER-CONFIRMATION' tras pedir confirmación al usuario. "
|
||||
"Genera run_id automático y registra en script_audit para rollback."
|
||||
)
|
||||
)
|
||||
def sync_missing_contacts(
|
||||
contact_ids: list[str] | None = None,
|
||||
apply: bool = False,
|
||||
confirm_token: str | None = None,
|
||||
) -> dict:
|
||||
err = _require_confirm(apply, confirm_token)
|
||||
if err:
|
||||
return {"ok": False, "error": err}
|
||||
result = adapters.sync_missing_contacts_adapter(
|
||||
contact_ids=contact_ids, dry_run=not apply
|
||||
)
|
||||
return adapters.maybe_offload("sync_missing_contacts", result)
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Sincroniza oportunidades faltantes de sucursales hacia Marca. "
|
||||
"Default dry_run=True. Para apply=True requiere confirm_token. "
|
||||
"Genera run_id y registra en script_audit para rollback."
|
||||
)
|
||||
)
|
||||
def sync_missing_opps(
|
||||
opp_ids: list[str] | None = None,
|
||||
apply: bool = False,
|
||||
confirm_token: str | None = None,
|
||||
) -> dict:
|
||||
err = _require_confirm(apply, confirm_token)
|
||||
if err:
|
||||
return {"ok": False, "error": err}
|
||||
result = adapters.sync_missing_opps_adapter(
|
||||
opp_ids=opp_ids, dry_run=not apply
|
||||
)
|
||||
return adapters.maybe_offload("sync_missing_opps", result)
|
||||
|
||||
# ---------- Observabilidad ----------
|
||||
|
||||
@mcp.tool(description="Logs recientes de sincronización (última corrida por sucursal). Read-only.")
|
||||
def sync_logs(limit: int = 20) -> dict:
|
||||
return {"ok": True, "logs": adapters.get_sync_logs_adapter(limit=limit)}
|
||||
|
||||
@mcp.tool(description="Errores recientes (tabla error_log + errors.jsonl). Read-only.")
|
||||
def error_logs(limit: int = 50) -> dict:
|
||||
return adapters.maybe_offload("error_logs", adapters.get_error_logs_adapter(limit=limit))
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Reporte de salud agentica: scripts, endpoints, issues. "
|
||||
"Regenera si stale_seconds>0 o si no existe. Lectura del JSON generado por scripts/audit_agent_readiness.py."
|
||||
)
|
||||
)
|
||||
def agent_audit_report() -> dict:
|
||||
report = adapters.script_catalog()
|
||||
return adapters.maybe_offload("agent_audit_report", report)
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Catálogo de tools y scripts disponibles (lectura del tools_manifest.json). "
|
||||
"Útil al inicio de una sesión para saber qué herramientas existen."
|
||||
)
|
||||
)
|
||||
def script_catalog() -> dict:
|
||||
path = os.path.join(os.path.dirname(__file__), "..", "generated", "agent", "tools_manifest.json")
|
||||
path = os.path.abspath(path)
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
return adapters.maybe_offload("script_catalog", manifest)
|
||||
|
||||
# ---------- Genérica (cola larga) ----------
|
||||
|
||||
@mcp.tool(
|
||||
description=(
|
||||
"Ejecuta cualquier script de scripts/ por subprocess. "
|
||||
"Para scripts mutadores debes pasar apply=True + confirm_token, y los args correctos del script (típicamente --apply --run-id ...). "
|
||||
"Si expect_json=True intenta parsear stdout como JSON y vuelca a generated/agent/runs/ si es grande. "
|
||||
"Lista de scripts disponibles en `script_catalog` / `agent_audit_report`."
|
||||
)
|
||||
)
|
||||
def run_script(
|
||||
name: str,
|
||||
args: list[str] | None = None,
|
||||
expect_json: bool = False,
|
||||
apply: bool = False,
|
||||
confirm_token: str | None = None,
|
||||
timeout_sec: int = 600,
|
||||
) -> dict:
|
||||
err = _require_confirm(apply, confirm_token)
|
||||
if err:
|
||||
return {"ok": False, "error": err}
|
||||
return adapters.run_python_script(
|
||||
name, args=args, timeout=timeout_sec, expect_json=expect_json
|
||||
)
|
||||
|
||||
return mcp
|
||||
Reference in New Issue
Block a user