overgrowth / agent /mcp_client.py
Graham Paasch
Import Overgrowth app
b3d02a4
Raw
History Blame
4.01 kB
from typing import List, Dict, Tuple
import os
from datetime import datetime, timezone
import requests
from .oob_tools.utils import load_json, save_json
# You can override this in HF Space secrets or env.
NCS_BASE_URL = os.getenv(
"NCS_BASE_URL",
"https://MCP-1st-Birthday-network-change-simulator.hf.space",
)
HISTORY_PATH = os.path.join(
os.path.dirname(__file__), "..", "infra", "mcp_history.json"
)
def _timestamp() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _record_history(device: str, preset: str, risk: Dict):
history = load_json(HISTORY_PATH) or {"recent": []}
recent = history.get("recent", [])
entry = {
"timestamp": _timestamp(),
"device": device,
"preset": preset,
"risk": risk.get("level"),
"score": risk.get("score"),
}
recent.append(entry)
# Keep history short to avoid bloat.
history["recent"] = recent[-50:]
save_json(HISTORY_PATH, history)
def _simulate_single_step(step: Dict) -> Tuple[Dict, Dict]:
"""
Call the Network Change Simulator MCP-style HTTP endpoint for one step.
Expects step["json"] to have:
- "change_type": "vlan" | "interface" | "bgp_neighbor"
- "preset_id": one of the known presets
- optional "mode": "lightning" | "full"
"""
payload = {
"change_type": step["json"]["change_type"],
"preset_id": step["json"]["preset_id"],
"mode": step["json"].get("mode", "lightning"),
}
url = f"{NCS_BASE_URL.rstrip('/')}/mcp/simulate"
try:
resp = requests.post(url, json=payload, timeout=10)
resp.raise_for_status()
except Exception as exc: # broad on purpose to keep UI resilient
# Fall back to a degraded but non-crashing experience.
fake = {
"risk_score": 50,
"risk_level": "medium",
"pre_check_summary": ["Stubbed due to MCP call failure."],
"post_check_summary": [],
"blast_radius_summary": "MCP call failed; using placeholder blast radius.",
"explanation": f"Error calling MCP server: {exc}",
"mode": payload["mode"],
"mode_note": "Simulated response because remote MCP call failed.",
}
tool_call = {
"tool": "simulate_network_change",
"arguments": payload,
"response": fake,
"error": str(exc),
}
risk = {
"level": fake["risk_level"],
"score": fake["risk_score"],
"summary": fake["explanation"],
}
return risk, tool_call
data = resp.json()
# Normalize risk structure for pipeline.
risk = {
"level": data.get("risk_level", "unknown"),
"score": data.get("risk_score", 0),
"summary": data.get("explanation", ""),
"blast_radius_summary": data.get("blast_radius_summary", ""),
"pre_check_summary": data.get("pre_check_summary", []),
"post_check_summary": data.get("post_check_summary", []),
"mode": data.get("mode"),
"mode_note": data.get("mode_note"),
}
tool_call = {
"tool": "simulate_network_change",
"arguments": payload,
"response": data,
}
return risk, tool_call
def simulate_steps_with_mcp(steps: List[Dict]) -> Tuple[List[Dict], List[Dict]]:
"""
For each step, call the Network Change Simulator MCP server.
Returns:
step_risks: list[dict]
tool_calls: list[dict] # for logging
"""
step_risks: List[Dict] = []
tool_calls: List[Dict] = []
for step in steps:
risk, call = _simulate_single_step(step)
step_risks.append(risk)
tool_calls.append(call)
# Persist history for downstream root-cause inference.
_record_history(
device=step["json"].get("device", "unknown"),
preset=step["json"].get("preset_id", "unknown"),
risk=risk,
)
return step_risks, tool_calls