Selaa lähdekoodia

Initial Hermes MCP scaffold

Lukas Goldschmidt 3 viikkoa sitten
vanhempi
commit
88c569fc7e

+ 16 - 0
PROJECT.md

@@ -0,0 +1,16 @@
+# Project
+
+## Goal
+Scaffold Hermes as a read-mostly orchestration layer sitting above Trader, Crypto, Metals, and News.
+
+## v1 shape
+- one MCP tool: `report()`
+- embedded dashboard on the same FastAPI port
+- `/health` endpoint
+- SQLite persistence
+- household scripts for run, kill, restart
+
+## Notes
+- Keep the surface tiny.
+- Favor explainable state over hidden logic.
+- Make the dashboard split into overview and technical monitoring.

+ 19 - 0
README.md

@@ -0,0 +1,19 @@
+# Hermes MCP
+
+Hermes MCP is a small FastAPI + MCP server for state, interpretation, and dashboarding.
+
+## Surface
+- MCP transport: `/mcp/sse`
+- Health: `/health`
+- Dashboard: `/dashboard/`
+- Tool: `report()`
+
+## Run
+```bash
+./run.sh 8590
+```
+
+## Test
+```bash
+./tests.sh
+```

+ 15 - 0
killserver.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+PORT="${1:-8590}"
+
+PIDS="$(lsof -ti tcp:"$PORT" 2>/dev/null || true)"
+if [ -n "${PIDS}" ]; then
+  kill ${PIDS} 2>/dev/null || true
+  sleep 1
+fi
+
+PIDS="$(lsof -ti tcp:"$PORT" 2>/dev/null || true)"
+if [ -n "${PIDS}" ]; then
+  kill -9 ${PIDS} 2>/dev/null || true
+fi

+ 20 - 0
pyproject.toml

@@ -0,0 +1,20 @@
+[build-system]
+requires = ["setuptools>=68", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "hermes-mcp"
+version = "0.1.0"
+description = "Hermes MCP server"
+requires-python = ">=3.11"
+dependencies = [
+  "fastapi>=0.115",
+  "uvicorn[standard]>=0.30",
+  "mcp>=1.0.0",
+]
+
+[tool.setuptools]
+package-dir = {"" = "src"}
+
+[tool.setuptools.packages.find]
+where = ["src"]

+ 4 - 0
requirements.txt

@@ -0,0 +1,4 @@
+fastapi>=0.115
+uvicorn[standard]>=0.30
+mcp>=1.0.0
+pytest>=8.0

+ 7 - 0
restart.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+PORT="${1:-8590}"
+
+./killserver.sh "$PORT"
+./run.sh "$PORT"

+ 17 - 0
run.sh

@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+PORT="${1:-8590}"
+
+if [ -f .venv/bin/activate ]; then
+  # shellcheck disable=SC1091
+  source .venv/bin/activate
+fi
+
+export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src:$(pwd)"
+mkdir -p ./logs
+
+uvicorn hermes_mcp.main:app --host 0.0.0.0 --port "$PORT" > ./logs/server.log 2>&1 &
+PID=$!
+echo "$PID" > ./logs/server.pid
+echo "Hermes MCP running on port $PORT (pid $PID)"

+ 3 - 0
scripts/household/README.md

@@ -0,0 +1,3 @@
+# Household scripts
+
+Small local helpers for Hermes maintenance and operator workflows.

+ 3 - 0
scripts/household/dashboard.sh

@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+set -euo pipefail
+printf 'Open: %s\n' "${1:-http://127.0.0.1:8590/dashboard/}"

+ 3 - 0
scripts/household/health.sh

@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+set -euo pipefail
+curl -fsS "${1:-http://127.0.0.1:8590/health}"

+ 1 - 0
src/hermes_mcp/__init__.py

@@ -0,0 +1 @@
+"""Hermes MCP package."""

BIN
src/hermes_mcp/__pycache__/__init__.cpython-313.pyc


BIN
src/hermes_mcp/__pycache__/dashboard.cpython-313.pyc


BIN
src/hermes_mcp/__pycache__/main.cpython-313.pyc


BIN
src/hermes_mcp/__pycache__/server.cpython-313.pyc


BIN
src/hermes_mcp/__pycache__/store.cpython-313.pyc


+ 29 - 0
src/hermes_mcp/dashboard.py

@@ -0,0 +1,29 @@
+from fastapi import APIRouter
+from fastapi.responses import HTMLResponse
+
+router = APIRouter(prefix="/dashboard", tags=["dashboard"])
+
+
+@router.get("/", response_class=HTMLResponse)
+def overview():
+    return """
+    <html><head><title>Hermes Dashboard</title></head>
+    <body>
+      <h1>Hermes</h1>
+      <p>Overview, signals, features, narrative, decision, explanation.</p>
+      <ul>
+        <li><a href="/dashboard/tech">Tech monitor</a></li>
+      </ul>
+    </body></html>
+    """
+
+
+@router.get("/tech", response_class=HTMLResponse)
+def tech():
+    return """
+    <html><head><title>Hermes Tech Monitor</title></head>
+    <body>
+      <h1>Tech Monitor</h1>
+      <p>Signals, features, state, narrative, decision, explanation.</p>
+    </body></html>
+    """

+ 4 - 0
src/hermes_mcp/main.py

@@ -0,0 +1,4 @@
+from .dashboard import router as dashboard_router
+from .server import app
+
+app.include_router(dashboard_router)

+ 46 - 0
src/hermes_mcp/server.py

@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI
+from mcp.server.fastmcp import FastMCP
+from mcp.server.transport_security import TransportSecuritySettings
+
+from .store import get_state, init_db
+
+mcp = FastMCP(
+    "hermes-mcp",
+    transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False),
+)
+
+
+@mcp.tool(description="Return Hermes current state, narrative, uncertainty, and a short self-assessment report.")
+def report() -> dict:
+    state = get_state()
+    return {
+        "status": state.get("status", "stub"),
+        "thinking": state.get("thinking", "Hermes scaffold is ready."),
+        "confidence": state.get("confidence", 0.0),
+        "uncertainty": state.get("uncertainty", ["no live adapters wired yet"]),
+        "layers": state.get("layers", []),
+    }
+
+
+@asynccontextmanager
+async def lifespan(_: FastAPI):
+    init_db()
+    yield
+
+
+app = FastAPI(title="Hermes MCP", lifespan=lifespan)
+app.mount("/mcp", mcp.sse_app())
+
+
+@app.get("/")
+def root() -> dict:
+    return {"status": "ok", "mount": "/mcp/sse", "dashboard": "/dashboard"}
+
+
+@app.get("/health")
+def health() -> dict:
+    return {"status": "ok", "db": "sqlite", "tool": "report"}

+ 52 - 0
src/hermes_mcp/store.py

@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import json
+import sqlite3
+from pathlib import Path
+from typing import Any
+
+ROOT = Path(__file__).resolve().parents[2]
+DATA_DIR = ROOT / "data"
+DB_PATH = DATA_DIR / "hermes_mcp.sqlite3"
+
+
+def _connect() -> sqlite3.Connection:
+    DATA_DIR.mkdir(parents=True, exist_ok=True)
+    conn = sqlite3.connect(DB_PATH)
+    conn.row_factory = sqlite3.Row
+    return conn
+
+
+def init_db() -> None:
+    with _connect() as conn:
+        conn.execute(
+            """
+            create table if not exists state (
+              key text primary key,
+              value text not null,
+              updated_at text not null default current_timestamp
+            )
+            """
+        )
+
+
+def get_state() -> dict[str, Any]:
+    init_db()
+    with _connect() as conn:
+        row = conn.execute("select value from state where key = ?", ("snapshot",)).fetchone()
+        if not row:
+            return {
+                "status": "stub",
+                "thinking": "Hermes is scaffolded and waiting for integrations.",
+                "layers": ["overview", "signals", "features", "narrative", "decision", "explanation"],
+            }
+        return json.loads(row["value"])
+
+
+def put_state(payload: dict[str, Any]) -> None:
+    init_db()
+    with _connect() as conn:
+        conn.execute(
+            "insert into state(key, value, updated_at) values(?, ?, current_timestamp) on conflict(key) do update set value=excluded.value, updated_at=current_timestamp",
+            ("snapshot", json.dumps(payload)),
+        )

+ 16 - 0
tests.sh

@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src:$(pwd)"
+
+if [ -f .venv/bin/activate ]; then
+  # shellcheck disable=SC1091
+  source .venv/bin/activate
+fi
+
+echo "Running unit pytest suite (hermes-mcp)..."
+if command -v pytest >/dev/null 2>&1; then
+  pytest -q
+else
+  python3 -m pytest -q
+fi

BIN
tests/__pycache__/test_report.cpython-313-pytest-9.0.3.pyc


BIN
tests/__pycache__/test_smoke.cpython-313-pytest-9.0.3.pyc


+ 7 - 0
tests/test_report.py

@@ -0,0 +1,7 @@
+from hermes_mcp.server import report
+
+
+def test_report_stub():
+    payload = report()
+    assert payload["status"] == "stub"
+    assert payload["layers"]

+ 15 - 0
tests/test_smoke.py

@@ -0,0 +1,15 @@
+from hermes_mcp.server import app
+
+
+def test_root():
+    from fastapi.testclient import TestClient
+
+    client = TestClient(app)
+    assert client.get("/").status_code == 200
+
+
+def test_health():
+    from fastapi.testclient import TestClient
+
+    client = TestClient(app)
+    assert client.get("/health").json()["status"] == "ok"