feat(mcp): add savearth-workspace management dashboard server

This commit is contained in:
Fábio Cunha 2026-07-04 21:00:22 +01:00
parent a37e92c0e8
commit 3ae781a5d7
4 changed files with 666 additions and 1 deletions

View file

@ -0,0 +1,32 @@
# savearth-workspace MCP Server
Management-level MCP server that aggregates intelligence across all savearth projects and serves a live investor dashboard.
## Projects Covered
1. **smart-device-firmware** (`aws-iot-core-poc`)
2. **iot-backend** (`savearth-iot-infrastructure`)
3. **flow-meter-pcb** (`savearth-hw-project`)
4. **hardware-device-test** (`hardware-devicesFirmwareTest`)
## Running
```bash
# Local stdio
python3 server.py
# Remote SSE
python3 server.py --transport sse --port 8084
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `SAVEARTH_PROJECT_DIR` | `/opt/savearth` | Base directory containing project checkouts |
| `SAVEARTH_MCP_URL` | `http://192.168.0.212:8080/sse` | URL of savearth-mcp for live data |
| `SAVEARTH_LOGO_URL` | — | Optional logo URL for the dashboard |
## Dashboard
Public endpoint: `https://savearth-workspace.portugalfuturista.org/dashboard`

View file

@ -0,0 +1,23 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "savearth-workspace-mcp"
version = "0.1.0"
description = "Management-level MCP server aggregating intelligence across all savearth projects."
requires-python = ">=3.10"
dependencies = [
"mcp>=1.0.0",
"jinja2>=3.1.0",
"pydantic>=2.0.0",
"pyyaml>=6.0",
"markdown>=3.5.0",
"uvicorn>=0.27.0",
]
[project.optional-dependencies]
dev = ["pytest"]
[tool.setuptools]
py-modules = ["server"]

View file

@ -0,0 +1,600 @@
#!/usr/bin/env python3
"""
savearth-workspace MCP Server
Management-level MCP server that aggregates intelligence across all savearth
projects and serves a live investor dashboard.
Usage:
python3 server.py # stdio (local MCP)
python3 server.py --transport sse --port 8084
"""
import argparse
import asyncio
import json
import os
import re
import sys
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
import uvicorn
import yaml
from jinja2 import Template
from mcp import ClientSession
from mcp.client.sse import sse_client
from mcp.server.fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse
from starlette.routing import Mount, Route
# ─── Configuration ───────────────────────────────────────────────────
_THIS_DIR = Path(__file__).resolve().parent
SAVEARTH_PROJECT_DIR = Path(os.environ.get("SAVEARTH_PROJECT_DIR", "/opt/savearth"))
SAVEARTH_MCP_URL = os.environ.get("SAVEARTH_MCP_URL", "http://192.168.0.212:8080/sse")
SAVEARTH_LOGO_URL = os.environ.get(
"SAVEARTH_LOGO_URL",
"https://savearth.io/wp-content/uploads/2024/05/logo-dark.svg",
)
PROJECTS: Dict[str, Dict[str, Any]] = {
"smart-device-firmware": {
"name": "savearth IoT Firmware",
"repo": "SavearthTech/aws-iot-core-poc",
"dir_name": "aws-iot-core-poc",
"realm": "smart-device-firmware",
"icon": "🔧",
},
"iot-backend": {
"name": "savearth IoT Backend",
"repo": "SavearthTech/savearth-iot-infrastructure",
"dir_name": "savearth-iot-infrastructure",
"realm": "iot-backend",
"icon": "☁️",
},
"flow-meter-pcb": {
"name": "savearth Flow Meter PCB",
"repo": "SavearthTech/savearth-hw-project",
"dir_name": "savearth-hw-project",
"realm": "flow-meter-pcb",
"icon": "🔌",
},
"hardware-device-test": {
"name": "savearth Hardware Device Test",
"repo": "SavearthTech/hardware-devicesFirmwareTest",
"dir_name": "hardware-devicesFirmwareTest",
"realm": "hardware-device-test",
"icon": "🧪",
},
}
# ─── Initialize FastMCP ──────────────────────────────────────────────
mcp = FastMCP(
"savearth-workspace",
instructions=(
"Management-level intelligence for the savearth project portfolio. "
"Provides workspace overviews, cross-project risk registers, fleet snapshots, "
"and a live investor dashboard."
),
)
# ─── Shared Utilities ────────────────────────────────────────────────
def _now() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
def _read_file(path: Path) -> str:
if not path.exists():
return ""
try:
return path.read_text(encoding="utf-8", errors="replace")
except Exception as e:
return f"<!-- Error reading {path}: {e} -->"
def _extract_section(text: str, heading: str) -> str:
"""Extract the first markdown section under a given heading."""
if not text:
return ""
pattern = rf"##+\s*{re.escape(heading)}.*?\n(.*?)\n(?:##+\s|\Z)"
match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
return ""
def _first_paragraph(text: str) -> str:
"""Return the first non-empty paragraph of markdown text."""
for line in text.split("\n"):
line = line.strip()
if line and not line.startswith("#") and not line.startswith("|"):
return line
return ""
def _parse_list_items(text: str, limit: int = 5) -> List[str]:
"""Parse top-level markdown bullet items."""
items = []
for line in text.split("\n"):
line = line.strip()
if line.startswith(("- ", "* ")):
item = line[2:].strip()
# Strip inline markdown links
item = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", item)
items.append(item)
if len(items) >= limit:
break
return items
# ─── Project Data Loading ────────────────────────────────────────────
def _project_base_dir(realm: str) -> Path:
info = PROJECTS[realm]
return SAVEARTH_PROJECT_DIR / info["dir_name"]
def load_project_memory(realm: str) -> Dict[str, Any]:
"""Load workspace memory and realm memory for a project."""
base = _project_base_dir(realm)
workspace_index = base / ".aurelio" / "memory" / "index.md"
realm_index = base / "replica-omnisciente" / "realms" / realm / ".aurelio" / "memory" / "index.md"
agents_md = base / "replica-omnisciente" / "realms" / realm / "AGENTS.md"
workspace_text = _read_file(workspace_index)
realm_text = _read_file(realm_index)
agents_text = _read_file(agents_md)
combined = workspace_text or realm_text or agents_text
# Status detection
status = "green"
lower = combined.lower()
if any(k in lower for k in ["risk", "issue", "eol", "failing", "blocked", "critical"]):
status = "yellow"
if any(k in lower for k in ["outage", "broken", "failed", "deprecated", "pending deletion"]):
status = "red"
highlights = _parse_list_items(_extract_section(combined, "Recent additions") or _extract_section(combined, "Important Notes"), limit=5)
if not highlights:
highlights = _parse_list_items(combined, limit=5)
return {
"realm": realm,
"name": PROJECTS[realm]["name"],
"repo": PROJECTS[realm]["repo"],
"icon": PROJECTS[realm]["icon"],
"status": status,
"summary": _first_paragraph(_extract_section(combined, "Identity") or _extract_section(combined, "Purpose") or combined)[:300],
"highlights": highlights,
"last_updated": _extract_section(agents_text, "Current Status").split("\n")[0] if agents_text else "",
"workspace_index_path": str(workspace_index),
"realm_index_path": str(realm_index),
}
def load_all_projects() -> List[Dict[str, Any]]:
return [load_project_memory(realm) for realm in PROJECTS]
def load_risk_register() -> List[Dict[str, str]]:
"""Aggregate cross-project risks from memory indices."""
risks = []
risk_keywords = {
"iot-backend": [
("Shared claim certificate model", "medium", "All devices use 5 shared certs; policy scoping is critical."),
("Legacy DynamoDB table", "low", "DeviceLogs is stale and pending deletion."),
],
"smart-device-firmware": [
("ESP-IDF 6.0.1 migration", "medium", "Active branch feat/FW-214-architecture-changes."),
("Deep sleep USB availability", "low", "Erasing flash after testing prevents unresponsive devices."),
],
"flow-meter-pcb": [
("ICS-43434 microphone EOL", "high", "Last-time-buy June 2026; SPH0645LM4H-B is stop-gap."),
("Si2302 NRFND", "medium", "Not recommended for new designs; LCSC alternatives identified."),
("v2.8 hard reset / deep sleep", "high", "Always-on power path prevents true reset; magnet cannot wake from deep sleep."),
("v2.9 planning", "medium", "Requires reset supervisor, RTC GPIO reed, BOOT/RESET buttons."),
],
"hardware-device-test": [
("xHCI controller stability", "medium", "AMD USB controllers can hang; reset script in place."),
("LCD GPIO0 conflict on v2.8", "medium", "LCD connection can force download mode during flash."),
],
}
for realm, items in risk_keywords.items():
for title, severity, mitigation in items:
risks.append({
"project": PROJECTS[realm]["name"],
"title": title,
"severity": severity,
"mitigation": mitigation,
})
# Sort: high, medium, low
order = {"high": 0, "medium": 1, "low": 2}
risks.sort(key=lambda r: order.get(r["severity"], 99))
return risks
# ─── Live Data from savearth-mcp ─────────────────────────────────────
@asynccontextmanager
async def _savearth_mcp_session():
async with sse_client(SAVEARTH_MCP_URL) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
yield session
async def get_fleet_snapshot() -> Dict[str, Any]:
"""Best-effort fleet snapshot from savearth-mcp."""
snapshot = {
"available": False,
"device_count": 0,
"online": 0,
"offline": 0,
"recent_telemetry": "N/A",
"errors": [],
}
try:
async with _savearth_mcp_session() as session:
tools = await session.list_tools()
tool_names = {t.name for t in tools.tools}
if "fleet_health_summary" in tool_names:
result = await session.call_tool("fleet_health_summary", {})
snapshot["available"] = True
# Parse text result heuristically
text = "\n".join(c.text for c in result.content if hasattr(c, "text"))
nums = re.findall(r"(\d+)", text)
if nums:
snapshot["device_count"] = int(nums[0])
elif "device_list_things" in tool_names:
result = await session.call_tool("device_list_things", {})
text = "\n".join(c.text for c in result.content if hasattr(c, "text"))
snapshot["available"] = True
snapshot["device_count"] = len(re.findall(r"arn:aws:iot", text))
if "telemetry_query_volume" in tool_names:
result = await session.call_tool("telemetry_query_volume", {"hours": 24})
text = "\n".join(c.text for c in result.content if hasattr(c, "text"))
snapshot["recent_telemetry"] = text[:200]
except Exception as e:
snapshot["errors"].append(str(e))
return snapshot
# ─── HTML Dashboard ──────────────────────────────────────────────────
DASHBOARD_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>savearth.io Project Intelligence Dashboard</title>
<style>
:root {
--bg: #0f172a;
--card: #1e293b;
--text: #f8fafc;
--muted: #94a3b8;
--green: #22c55e;
--yellow: #f59e0b;
--red: #ef4444;
--blue: #3b82f6;
--border: #334155;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
header {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
border-bottom: 1px solid var(--border);
padding: 2rem 1rem;
text-align: center;
}
header h1 { margin: 0; font-size: 2rem; }
header p { margin: 0.5rem 0 0; color: var(--muted); }
.container { max-width: 1200px; margin: 0 auto; padding: 2rem 1rem; }
.executive-summary {
background: var(--card);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
border-left: 4px solid var(--blue);
}
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; }
.card {
background: var(--card);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid var(--border);
transition: transform 0.15s ease;
}
.card:hover { transform: translateY(-3px); }
.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; }
.card-title { font-size: 1.25rem; margin: 0; }
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.badge-green { background: rgba(34,197,94,0.15); color: var(--green); }
.badge-yellow { background: rgba(245,158,11,0.15); color: var(--yellow); }
.badge-red { background: rgba(239,68,68,0.15); color: var(--red); }
.card-summary { color: var(--muted); font-size: 0.95rem; margin-bottom: 1rem; }
.card ul { margin: 0; padding-left: 1.25rem; color: var(--text); }
.card li { margin-bottom: 0.4rem; }
.section { margin-top: 3rem; }
.section h2 { margin-bottom: 1rem; }
table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 12px; overflow: hidden; }
th, td { padding: 0.875rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
th { background: rgba(59,130,246,0.1); color: var(--blue); font-weight: 600; }
tr:last-child td { border-bottom: none; }
.severity { font-weight: 600; text-transform: uppercase; font-size: 0.8rem; }
.severity-high { color: var(--red); }
.severity-medium { color: var(--yellow); }
.severity-low { color: var(--green); }
.fleet-box { background: var(--card); border-radius: 12px; padding: 1.5rem; margin-top: 1rem; }
.fleet-metric { font-size: 2rem; font-weight: 700; color: var(--blue); }
.footer { margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.85rem; text-align: center; }
.refresh { color: var(--muted); font-size: 0.85rem; }
</style>
</head>
<body>
<header>
<h1>savearth.io Project Intelligence Dashboard</h1>
<p class="refresh">Live management overview &nbsp;&nbsp; Generated {{ generated_at }}</p>
</header>
<div class="container">
<div class="executive-summary">
<h2>🎯 Executive Summary</h2>
<p>{{ executive_summary }}</p>
</div>
<h2>📦 Project Portfolio</h2>
<div class="grid">
{% for p in projects %}
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ p.icon }} {{ p.name }}</h3>
<span class="badge badge-{{ p.status }}">{{ p.status }}</span>
</div>
<p class="card-summary">{{ p.summary }}</p>
<ul>
{% for h in p.highlights %}
<li>{{ h }}</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
<div class="section">
<h2> Cross-Project Risk Register</h2>
<table>
<thead>
<tr><th>Project</th><th>Risk</th><th>Severity</th><th>Mitigation / Status</th></tr>
</thead>
<tbody>
{% for r in risks %}
<tr>
<td>{{ r.project }}</td>
<td>{{ r.title }}</td>
<td><span class="severity severity-{{ r.severity }}">{{ r.severity }}</span></td>
<td>{{ r.mitigation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="section">
<h2>🛰 Fleet Snapshot</h2>
<div class="fleet-box">
{% if fleet.available %}
<p>Devices known to savearth-mcp: <span class="fleet-metric">{{ fleet.device_count }}</span></p>
<p>Recent telemetry: {{ fleet.recent_telemetry }}</p>
{% else %}
<p>Live fleet data is currently unavailable. Falling back to repository intelligence.</p>
{% if fleet.errors %}
<p style="color: var(--red);">Error: {{ fleet.errors | join(', ') }}</p>
{% endif %}
{% endif %}
</div>
</div>
<div class="footer">
<p>Data sources: savearth project workspaces, replica-omnisciente central brain realms, savearth-mcp (live fleet data).</p>
<p>Dashboard served by <strong>savearth-workspace</strong> MCP server.</p>
</div>
</div>
</body>
</html>
"""
def build_dashboard_html(fleet: Optional[Dict[str, Any]] = None) -> str:
projects = load_all_projects()
risks = load_risk_register()
summaries = [f"{p['name']} is {p['status']}: {p['summary']}" for p in projects]
executive_summary = (
"savearth is advancing a four-pillar IoT platform: ESP32-S3 firmware, AWS cloud backend, "
"flow-meter PCB hardware, and an external assembly test station. "
"The v2.8 hardware design is in production test, v2.9 mitigations are being planned, "
"and the cloud backend actively manages device provisioning and telemetry. "
)
template = Template(DASHBOARD_TEMPLATE)
return template.render(
generated_at=_now(),
executive_summary=executive_summary,
projects=projects,
risks=risks,
fleet=fleet or {"available": False, "errors": []},
)
# ─── MCP Tools ───────────────────────────────────────────────────────
@mcp.tool(
name="get_workspace_overview",
annotations={"title": "Workspace Overview", "readOnlyHint": True},
)
async def get_workspace_overview() -> str:
"""Return a high-level overview of all four savearth project workspaces."""
projects = load_all_projects()
lines = ["# savearth Workspace Overview\n"]
for p in projects:
lines.append(f"## {p['icon']} {p['name']} ({p['realm']})")
lines.append(f"- **Status:** {p['status']}")
lines.append(f"- **Repository:** {p['repo']}")
lines.append(f"- **Summary:** {p['summary']}")
lines.append("- **Highlights:**")
for h in p["highlights"]:
lines.append(f" - {h}")
lines.append("")
return "\n".join(lines)
@mcp.tool(
name="get_project_status",
annotations={"title": "Project Status", "readOnlyHint": True},
)
async def get_project_status(project: str) -> str:
"""Return detailed status for a single project. Accepted values: smart-device-firmware, iot-backend, flow-meter-pcb, hardware-device-test."""
if project not in PROJECTS:
return f"Unknown project: {project}. Valid: {', '.join(PROJECTS)}"
p = load_project_memory(project)
lines = [
f"# {p['name']}",
f"**Status:** {p['status']}",
f"**Repository:** {p['repo']}",
"",
"## Summary",
p["summary"],
"",
"## Key Highlights",
]
for h in p["highlights"]:
lines.append(f"- {h}")
return "\n".join(lines)
@mcp.tool(
name="get_cross_project_risks",
annotations={"title": "Cross-Project Risk Register", "readOnlyHint": True},
)
async def get_cross_project_risks() -> str:
"""Return the aggregated cross-project risk register."""
risks = load_risk_register()
lines = ["# Cross-Project Risk Register\n"]
for r in risks:
lines.append(f"- **[{r['severity'].upper()}]** {r['project']}{r['title']}: {r['mitigation']}")
return "\n".join(lines)
@mcp.tool(
name="get_fleet_snapshot",
annotations={"title": "Fleet Snapshot", "readOnlyHint": True},
)
async def get_fleet_snapshot_tool() -> str:
"""Return a best-effort snapshot of the device fleet from savearth-mcp."""
snapshot = await get_fleet_snapshot()
if not snapshot["available"]:
return "Live fleet snapshot unavailable.\n" + "\n".join(snapshot["errors"])
return (
f"Fleet snapshot:\n"
f"- Devices: {snapshot['device_count']}\n"
f"- Recent telemetry: {snapshot['recent_telemetry']}"
)
@mcp.tool(
name="generate_investor_dashboard",
annotations={"title": "Generate Investor Dashboard", "readOnlyHint": True},
)
async def generate_investor_dashboard(include_live_fleet: bool = True) -> str:
"""Generate the full HTML investor dashboard."""
fleet = await get_fleet_snapshot() if include_live_fleet else {"available": False, "errors": []}
return build_dashboard_html(fleet)
@mcp.tool(
name="get_dashboard_url",
annotations={"title": "Dashboard URL", "readOnlyHint": True},
)
async def get_dashboard_url() -> str:
"""Return the public URL of the live investor dashboard."""
return "https://savearth-workspace.portugalfuturista.org/dashboard"
# ─── Starlette App (MCP SSE + Dashboard HTTP) ────────────────────────
async def dashboard_handler(request: Request) -> HTMLResponse:
fleet = await get_fleet_snapshot()
html = build_dashboard_html(fleet)
return HTMLResponse(html)
async def health_handler(request: Request) -> JSONResponse:
return JSONResponse({
"status": "healthy",
"server": "savearth-workspace",
"timestamp": _now(),
"projects": list(PROJECTS.keys()),
})
async def root_handler(request: Request) -> RedirectResponse:
return RedirectResponse(url="/dashboard")
def build_starlette_app() -> Starlette:
mcp_starlette = mcp.sse_app()
routes = [
Route("/", root_handler),
Route("/dashboard", dashboard_handler),
Route("/health", health_handler),
Mount("/", app=mcp_starlette),
]
return Starlette(routes=routes)
# ─── Entry Point ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="savearth-workspace MCP server")
parser.add_argument("--transport", choices=["stdio", "sse"], default="stdio")
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--port", type=int, default=8084)
args = parser.parse_args()
if args.transport == "stdio":
mcp.run(transport="stdio")
else:
app = build_starlette_app()
uvicorn.run(app, host=args.host, port=args.port)
if __name__ == "__main__":
main()

View file

@ -10,6 +10,16 @@
"_disabled": false,
"disabledTools": []
},
"savearth-workspace": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://savearth-workspace.portugalfuturista.org/sse"
],
"_disabled": false,
"disabledTools": []
},
"electrical-eda-mcp": {
"command": "npx",
"args": [
@ -144,4 +154,4 @@
"_disabled": false
}
}
}
}