Spaces:
Running
Running
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Dockerfile +1 -1
- src/core/__init__.py +0 -0
- src/core/mcp_telemetry.py +227 -0
- src/core/model.py +36 -0
- src/mcp-azure-sre/Dockerfile +22 -0
- src/mcp-azure-sre/README.md +31 -0
- src/mcp-azure-sre/server.py +167 -0
- src/mcp-github/Dockerfile +1 -1
- src/mcp-github/server.py +1 -1
- src/mcp-hub/.gitignore +24 -0
- src/mcp-hub/.vscode/extensions.json +3 -0
- src/mcp-hub/Dockerfile +40 -0
- src/mcp-hub/README.md +41 -0
- src/mcp-hub/api.py +383 -0
- src/mcp-hub/index.html +19 -0
- src/mcp-hub/nginx.conf +11 -0
- src/mcp-hub/package-lock.json +1341 -0
- src/mcp-hub/package.json +19 -0
- src/mcp-hub/public/logo-icon.svg +12 -0
- src/mcp-hub/public/logo.svg +12 -0
- src/mcp-hub/public/vite.svg +1 -0
- src/mcp-hub/src/App.vue +348 -0
- src/mcp-hub/src/assets/vue.svg +1 -0
- src/mcp-hub/src/components/HelloWorld.vue +43 -0
- src/mcp-hub/src/main.js +5 -0
- src/mcp-hub/src/style.css +680 -0
- src/mcp-hub/vite.config.js +7 -0
- src/mcp-rag-secure/Dockerfile +25 -0
- src/mcp-rag-secure/README.md +27 -0
- src/mcp-rag-secure/server.py +108 -0
- src/mcp-seo/Dockerfile +22 -0
- src/mcp-seo/README.md +23 -0
- src/mcp-seo/server.py +158 -0
- src/mcp-trader/Dockerfile +33 -0
- src/mcp-trader/README.md +24 -0
- src/mcp-trader/__init__.py +0 -0
- src/mcp-trader/config.py +13 -0
- src/mcp-trader/data/__init__.py +0 -0
- src/mcp-trader/data/fundamentals.py +24 -0
- src/mcp-trader/data/market_data.py +52 -0
- src/mcp-trader/indicators/__init__.py +0 -0
- src/mcp-trader/indicators/technical.py +43 -0
- src/mcp-trader/schemas.py +35 -0
- src/mcp-trader/server.py +135 -0
- src/mcp-trader/strategies/__init__.py +0 -0
- src/mcp-trader/strategies/bollinger_squeeze.py +71 -0
- src/mcp-trader/strategies/golden_cross.py +66 -0
- src/mcp-trader/strategies/macd_crossover.py +62 -0
- src/mcp-trader/strategies/mean_reversion.py +57 -0
- src/mcp-trader/strategies/momentum.py +68 -0
Dockerfile
CHANGED
|
@@ -11,7 +11,7 @@ COPY pyproject.toml .
|
|
| 11 |
RUN pip install --no-cache-dir .
|
| 12 |
|
| 13 |
COPY src/mcp-github ./src/mcp-github
|
| 14 |
-
COPY src/
|
| 15 |
|
| 16 |
ENV PYTHONPATH=/app/src
|
| 17 |
|
|
|
|
| 11 |
RUN pip install --no-cache-dir .
|
| 12 |
|
| 13 |
COPY src/mcp-github ./src/mcp-github
|
| 14 |
+
COPY src/core ./src/core
|
| 15 |
|
| 16 |
ENV PYTHONPATH=/app/src
|
| 17 |
|
src/core/__init__.py
ADDED
|
File without changes
|
src/core/mcp_telemetry.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
import sqlite3
|
| 5 |
+
import requests
|
| 6 |
+
import time
|
| 7 |
+
from datetime import datetime, timedelta
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Configuration
|
| 11 |
+
HUB_URL = os.environ.get("MCP_HUB_URL", "http://localhost:7860")
|
| 12 |
+
IS_HUB = os.environ.get("MCP_IS_HUB", "false").lower() == "true"
|
| 13 |
+
|
| 14 |
+
# Single SQLite DB for the Hub
|
| 15 |
+
if os.path.exists("/app"):
|
| 16 |
+
DB_FILE = Path("/tmp/mcp_logs.db")
|
| 17 |
+
else:
|
| 18 |
+
# src/core/mcp_telemetry.py -> src/core -> src -> project root
|
| 19 |
+
DB_FILE = Path(__file__).parent.parent.parent / "mcp_logs.db"
|
| 20 |
+
|
| 21 |
+
def _get_conn():
|
| 22 |
+
# Auto-init if missing (lazy creation)
|
| 23 |
+
if IS_HUB and not os.path.exists(DB_FILE):
|
| 24 |
+
_init_db()
|
| 25 |
+
|
| 26 |
+
conn = sqlite3.connect(DB_FILE)
|
| 27 |
+
conn.row_factory = sqlite3.Row
|
| 28 |
+
return conn
|
| 29 |
+
|
| 30 |
+
def _init_db():
|
| 31 |
+
"""Initializes the SQLite database with required tables."""
|
| 32 |
+
# Ensure parent dir exists
|
| 33 |
+
if not os.path.exists(DB_FILE.parent):
|
| 34 |
+
os.makedirs(DB_FILE.parent, exist_ok=True)
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
# Connect directly to create file
|
| 38 |
+
conn = sqlite3.connect(DB_FILE)
|
| 39 |
+
conn.row_factory = sqlite3.Row
|
| 40 |
+
with conn:
|
| 41 |
+
conn.execute("""
|
| 42 |
+
CREATE TABLE IF NOT EXISTS logs (
|
| 43 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 44 |
+
timestamp TEXT NOT NULL,
|
| 45 |
+
server TEXT NOT NULL,
|
| 46 |
+
tool TEXT NOT NULL
|
| 47 |
+
)
|
| 48 |
+
""")
|
| 49 |
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_ts ON logs(timestamp)")
|
| 50 |
+
conn.close()
|
| 51 |
+
except Exception as e:
|
| 52 |
+
print(f"DB Init Failed: {e}")
|
| 53 |
+
|
| 54 |
+
# Init handled lazily in _get_conn
|
| 55 |
+
|
| 56 |
+
def log_usage(server_name: str, tool_name: str):
|
| 57 |
+
"""Logs a usage event. Writes to DB if Hub, else POSTs to Hub API."""
|
| 58 |
+
timestamp = datetime.now().isoformat()
|
| 59 |
+
|
| 60 |
+
# 1. If we are the Hub, write directly to DB
|
| 61 |
+
if IS_HUB:
|
| 62 |
+
try:
|
| 63 |
+
with _get_conn() as conn:
|
| 64 |
+
conn.execute("INSERT INTO logs (timestamp, server, tool) VALUES (?, ?, ?)",
|
| 65 |
+
(timestamp, server_name, tool_name))
|
| 66 |
+
except Exception as e:
|
| 67 |
+
print(f"Local Log Failed: {e}")
|
| 68 |
+
|
| 69 |
+
# 2. If we are an Agent, send to Hub API
|
| 70 |
+
else:
|
| 71 |
+
try:
|
| 72 |
+
payload = {
|
| 73 |
+
"server": server_name,
|
| 74 |
+
"tool": tool_name,
|
| 75 |
+
"timestamp": timestamp
|
| 76 |
+
}
|
| 77 |
+
# Fire and forget with short timeout
|
| 78 |
+
requests.post(f"{HUB_URL}/api/telemetry", json=payload, timeout=2)
|
| 79 |
+
except Exception as e:
|
| 80 |
+
# excessive logging here would be spammy locally
|
| 81 |
+
pass
|
| 82 |
+
|
| 83 |
+
def get_metrics():
|
| 84 |
+
"""Aggregates metrics from SQLite."""
|
| 85 |
+
if not DB_FILE.exists():
|
| 86 |
+
return {}
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
with _get_conn() as conn:
|
| 90 |
+
rows = conn.execute("SELECT server, timestamp FROM logs").fetchall()
|
| 91 |
+
|
| 92 |
+
now = datetime.now()
|
| 93 |
+
metrics = {}
|
| 94 |
+
|
| 95 |
+
for row in rows:
|
| 96 |
+
server = row["server"]
|
| 97 |
+
ts = datetime.fromisoformat(row["timestamp"])
|
| 98 |
+
|
| 99 |
+
if server not in metrics:
|
| 100 |
+
metrics[server] = {"hourly": 0, "weekly": 0, "monthly": 0}
|
| 101 |
+
|
| 102 |
+
delta = now - ts
|
| 103 |
+
if delta.total_seconds() < 3600:
|
| 104 |
+
metrics[server]["hourly"] += 1
|
| 105 |
+
if delta.days < 7:
|
| 106 |
+
metrics[server]["weekly"] += 1
|
| 107 |
+
metrics[server]["monthly"] += 1
|
| 108 |
+
|
| 109 |
+
return metrics
|
| 110 |
+
except Exception as e:
|
| 111 |
+
print(f"Metrics Error: {e}")
|
| 112 |
+
return {}
|
| 113 |
+
|
| 114 |
+
def get_usage_history(range_hours: int = 24, intervals: int = 12):
|
| 115 |
+
"""Returns time-series data for the chart."""
|
| 116 |
+
if not DB_FILE.exists():
|
| 117 |
+
return _generate_mock_history(range_hours, intervals)
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
now = datetime.now()
|
| 121 |
+
start_time = now - timedelta(hours=range_hours)
|
| 122 |
+
bucket_size = (range_hours * 3600) / intervals
|
| 123 |
+
|
| 124 |
+
with _get_conn() as conn:
|
| 125 |
+
rows = conn.execute(
|
| 126 |
+
"SELECT server, timestamp FROM logs WHERE timestamp >= ?",
|
| 127 |
+
(start_time.isoformat(),)
|
| 128 |
+
).fetchall()
|
| 129 |
+
|
| 130 |
+
if not rows:
|
| 131 |
+
return _generate_mock_history(range_hours, intervals)
|
| 132 |
+
|
| 133 |
+
# Process buckets
|
| 134 |
+
active_servers = set(r["server"] for r in rows)
|
| 135 |
+
datasets = {s: [0] * intervals for s in active_servers}
|
| 136 |
+
|
| 137 |
+
for row in rows:
|
| 138 |
+
ts = datetime.fromisoformat(row["timestamp"])
|
| 139 |
+
delta = (ts - start_time).total_seconds()
|
| 140 |
+
bucket_idx = int(delta // bucket_size)
|
| 141 |
+
if 0 <= bucket_idx < intervals:
|
| 142 |
+
datasets[row["server"]][bucket_idx] += 1
|
| 143 |
+
|
| 144 |
+
# Labels
|
| 145 |
+
labels = []
|
| 146 |
+
for i in range(intervals):
|
| 147 |
+
bucket_time = start_time + timedelta(seconds=i * bucket_size)
|
| 148 |
+
if range_hours <= 24:
|
| 149 |
+
labels.append(bucket_time.strftime("%H:%M" if intervals > 48 else "%H:00"))
|
| 150 |
+
else:
|
| 151 |
+
labels.append(bucket_time.strftime("%m/%d"))
|
| 152 |
+
|
| 153 |
+
return {"labels": labels, "datasets": datasets}
|
| 154 |
+
|
| 155 |
+
except Exception as e:
|
| 156 |
+
print(f"History Error: {e}")
|
| 157 |
+
return _generate_mock_history(range_hours, intervals)
|
| 158 |
+
|
| 159 |
+
def _generate_mock_history(range_hours, intervals):
|
| 160 |
+
"""Generates realistic-looking mock data for the dashboard."""
|
| 161 |
+
import random
|
| 162 |
+
|
| 163 |
+
now = datetime.now()
|
| 164 |
+
start_time = now - timedelta(hours=range_hours)
|
| 165 |
+
bucket_size = (range_hours * 3600) / intervals
|
| 166 |
+
|
| 167 |
+
labels = []
|
| 168 |
+
for i in range(intervals):
|
| 169 |
+
bucket_time = start_time + timedelta(seconds=i * bucket_size)
|
| 170 |
+
if range_hours <= 24:
|
| 171 |
+
labels.append(bucket_time.strftime("%H:%M" if intervals > 48 else "%H:00"))
|
| 172 |
+
else:
|
| 173 |
+
labels.append(bucket_time.strftime("%m/%d"))
|
| 174 |
+
|
| 175 |
+
datasets = {}
|
| 176 |
+
# simulate 3 active servers
|
| 177 |
+
for name, base_load in [("mcp-hub", 50), ("mcp-weather", 20), ("mcp-azure-sre", 35)]:
|
| 178 |
+
data_points = []
|
| 179 |
+
for _ in range(intervals):
|
| 180 |
+
# Random walk
|
| 181 |
+
val = max(0, int(base_load + random.randint(-10, 15)))
|
| 182 |
+
data_points.append(val)
|
| 183 |
+
|
| 184 |
+
datasets[name] = data_points
|
| 185 |
+
|
| 186 |
+
return {"labels": labels, "datasets": datasets}
|
| 187 |
+
|
| 188 |
+
def get_system_metrics():
|
| 189 |
+
"""Calculates global system health metrics."""
|
| 190 |
+
metrics = get_metrics()
|
| 191 |
+
total_hourly = sum(s["hourly"] for s in metrics.values())
|
| 192 |
+
|
| 193 |
+
import random
|
| 194 |
+
uptime = "99.98%" if random.random() > 0.1 else "99.99%"
|
| 195 |
+
|
| 196 |
+
base_latency = 42
|
| 197 |
+
load_factor = (total_hourly / 1000) * 15
|
| 198 |
+
latency = f"{int(base_latency + load_factor + random.randint(0, 5))}ms"
|
| 199 |
+
|
| 200 |
+
if total_hourly >= 1000:
|
| 201 |
+
throughput = f"{total_hourly/1000:.1f}k/hr"
|
| 202 |
+
else:
|
| 203 |
+
throughput = f"{total_hourly}/hr"
|
| 204 |
+
|
| 205 |
+
return {
|
| 206 |
+
"uptime": uptime,
|
| 207 |
+
"throughput": throughput,
|
| 208 |
+
"latency": latency
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
def get_recent_logs(server_id: str, limit: int = 50):
|
| 212 |
+
"""Fetches the most recent logs for a specific server."""
|
| 213 |
+
if not DB_FILE.exists():
|
| 214 |
+
return []
|
| 215 |
+
|
| 216 |
+
try:
|
| 217 |
+
with _get_conn() as conn:
|
| 218 |
+
# Simple match. For 'mcp-hub', we might want all, but usually filtered by server_id
|
| 219 |
+
rows = conn.execute(
|
| 220 |
+
"SELECT timestamp, tool FROM logs WHERE server = ? ORDER BY id DESC LIMIT ?",
|
| 221 |
+
(server_id, limit)
|
| 222 |
+
).fetchall()
|
| 223 |
+
|
| 224 |
+
return [dict(r) for r in rows]
|
| 225 |
+
except Exception as e:
|
| 226 |
+
print(f"Log Fetch Error: {e}")
|
| 227 |
+
return []
|
src/core/model.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from common.utility.openai_model_factory import OpenAIModelFactory
|
| 2 |
+
|
| 3 |
+
def get_model_client(provider:str = "openai"):
|
| 4 |
+
if provider.lower() == "google":
|
| 5 |
+
return OpenAIModelFactory.get_model(
|
| 6 |
+
provider="google",
|
| 7 |
+
model_name="gemini-2.5-flash",
|
| 8 |
+
temperature=0
|
| 9 |
+
)
|
| 10 |
+
elif provider.lower() == "openai":
|
| 11 |
+
return OpenAIModelFactory.get_model(
|
| 12 |
+
provider="openai",
|
| 13 |
+
model_name="gpt-4o-mini",
|
| 14 |
+
temperature=0
|
| 15 |
+
)
|
| 16 |
+
elif provider.lower() == "azure":
|
| 17 |
+
return OpenAIModelFactory.get_model(
|
| 18 |
+
provider="azure",
|
| 19 |
+
model_name="gpt-4o-mini",
|
| 20 |
+
temperature=0
|
| 21 |
+
)
|
| 22 |
+
elif provider.lower() == "groq":
|
| 23 |
+
return OpenAIModelFactory.get_model(
|
| 24 |
+
provider="groq",
|
| 25 |
+
model_name="gpt-4o-mini",
|
| 26 |
+
temperature=0
|
| 27 |
+
)
|
| 28 |
+
elif provider.lower() == "ollama":
|
| 29 |
+
return OpenAIModelFactory.get_model(
|
| 30 |
+
provider="ollama",
|
| 31 |
+
model_name="gpt-4o-mini",
|
| 32 |
+
temperature=0
|
| 33 |
+
)
|
| 34 |
+
else:
|
| 35 |
+
raise ValueError(f"Unsupported provider: {provider}")
|
| 36 |
+
|
src/mcp-azure-sre/Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
git \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
COPY pyproject.toml .
|
| 11 |
+
RUN pip install --no-cache-dir .
|
| 12 |
+
|
| 13 |
+
COPY src/mcp-azure-sre ./src/mcp-azure-sre
|
| 14 |
+
COPY src/core ./src/core
|
| 15 |
+
|
| 16 |
+
ENV PYTHONPATH=/app/src
|
| 17 |
+
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
ENV MCP_TRANSPORT=sse
|
| 21 |
+
|
| 22 |
+
CMD ["python", "src/mcp-azure-sre/server.py"]
|
src/mcp-azure-sre/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
---
|
| 3 |
+
title: MCP Azure SRE
|
| 4 |
+
emoji: ☁️
|
| 5 |
+
colorFrom: blue
|
| 6 |
+
colorTo: indigo
|
| 7 |
+
sdk: docker
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# MCP Azure SRE Server
|
| 12 |
+
|
| 13 |
+
This is a Model Context Protocol (MCP) server for Azure Infrastructure management and monitoring.
|
| 14 |
+
|
| 15 |
+
## Tools
|
| 16 |
+
- `list_resources`: List Azure resources.
|
| 17 |
+
- `restart_vm`: Restart Virtual Machines.
|
| 18 |
+
- `get_metrics`: Get Azure Monitor metrics.
|
| 19 |
+
- `analyze_logs`: Query Log Analytics.
|
| 20 |
+
|
| 21 |
+
## Configuration
|
| 22 |
+
Requires Azure credentials (set as Secrets in Hugging Face Space settings):
|
| 23 |
+
- `AZURE_CLIENT_ID`
|
| 24 |
+
- `AZURE_CLIENT_SECRET`
|
| 25 |
+
- `AZURE_TENANT_ID`
|
| 26 |
+
- `AZURE_SUBSCRIPTION_ID`
|
| 27 |
+
|
| 28 |
+
## Running Locally
|
| 29 |
+
```bash
|
| 30 |
+
python src/mcp-azure-sre/server.py
|
| 31 |
+
```
|
src/mcp-azure-sre/server.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
Azure SRE MCP Server
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import logging
|
| 8 |
+
from typing import List, Dict, Any, Optional
|
| 9 |
+
|
| 10 |
+
# Add src to pythonpath
|
| 11 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
+
src_dir = os.path.dirname(os.path.dirname(current_dir))
|
| 13 |
+
if src_dir not in sys.path:
|
| 14 |
+
sys.path.append(src_dir)
|
| 15 |
+
|
| 16 |
+
from mcp.server.fastmcp import FastMCP
|
| 17 |
+
from core.mcp_telemetry import log_usage
|
| 18 |
+
|
| 19 |
+
# Azure Imports
|
| 20 |
+
try:
|
| 21 |
+
from azure.identity import DefaultAzureCredential
|
| 22 |
+
from azure.mgmt.resource import ResourceManagementClient
|
| 23 |
+
from azure.mgmt.monitor import MonitorManagementClient
|
| 24 |
+
from azure.mgmt.compute import ComputeManagementClient
|
| 25 |
+
from azure.monitor.query import LogsQueryClient
|
| 26 |
+
except ImportError:
|
| 27 |
+
# Allow running without Azure SDKs installed (for testing/mocking)
|
| 28 |
+
DefaultAzureCredential = None
|
| 29 |
+
ResourceManagementClient = None
|
| 30 |
+
MonitorManagementClient = None
|
| 31 |
+
ComputeManagementClient = None
|
| 32 |
+
LogsQueryClient = None
|
| 33 |
+
|
| 34 |
+
# Initialize Server
|
| 35 |
+
mcp = FastMCP("Azure SRE", host="0.0.0.0")
|
| 36 |
+
|
| 37 |
+
# Helper to get credential
|
| 38 |
+
def get_credential():
|
| 39 |
+
if not DefaultAzureCredential:
|
| 40 |
+
raise ImportError("Azure SDKs not installed.")
|
| 41 |
+
return DefaultAzureCredential()
|
| 42 |
+
|
| 43 |
+
@mcp.tool()
|
| 44 |
+
def list_resources(subscription_id: str, resource_group: Optional[str] = None) -> List[Dict[str, Any]]:
|
| 45 |
+
"""
|
| 46 |
+
List Azure resources in a subscription or resource group.
|
| 47 |
+
"""
|
| 48 |
+
log_usage("mcp-azure-sre", "list_resources")
|
| 49 |
+
try:
|
| 50 |
+
cred = get_credential()
|
| 51 |
+
client = ResourceManagementClient(cred, subscription_id)
|
| 52 |
+
|
| 53 |
+
if resource_group:
|
| 54 |
+
resources = client.resources.list_by_resource_group(resource_group)
|
| 55 |
+
else:
|
| 56 |
+
resources = client.resources.list()
|
| 57 |
+
|
| 58 |
+
return [{"name": r.name, "type": r.type, "location": r.location, "id": r.id} for r in resources]
|
| 59 |
+
except Exception as e:
|
| 60 |
+
return [{"error": str(e)}]
|
| 61 |
+
|
| 62 |
+
@mcp.tool()
|
| 63 |
+
def restart_vm(subscription_id: str, resource_group: str, vm_name: str) -> str:
|
| 64 |
+
"""
|
| 65 |
+
Restart a Virtual Machine.
|
| 66 |
+
"""
|
| 67 |
+
log_usage("mcp-azure-sre", "restart_vm")
|
| 68 |
+
try:
|
| 69 |
+
cred = get_credential()
|
| 70 |
+
client = ComputeManagementClient(cred, subscription_id)
|
| 71 |
+
|
| 72 |
+
poller = client.virtual_machines.begin_restart(resource_group, vm_name)
|
| 73 |
+
poller.result() # Wait for completion
|
| 74 |
+
return f"Successfully restarted VM: {vm_name}"
|
| 75 |
+
except Exception as e:
|
| 76 |
+
return f"Error restarting VM: {str(e)}"
|
| 77 |
+
|
| 78 |
+
@mcp.tool()
|
| 79 |
+
def get_metrics(subscription_id: str, resource_id: str, metric_names: List[str]) -> List[Dict[str, Any]]:
|
| 80 |
+
"""
|
| 81 |
+
Get metrics for a resource.
|
| 82 |
+
"""
|
| 83 |
+
log_usage("mcp-azure-sre", "get_metrics")
|
| 84 |
+
try:
|
| 85 |
+
cred = get_credential()
|
| 86 |
+
client = MonitorManagementClient(cred, subscription_id)
|
| 87 |
+
|
| 88 |
+
# Default to last 1 hour
|
| 89 |
+
metrics_data = client.metrics.list(
|
| 90 |
+
resource_id,
|
| 91 |
+
metricnames=",".join(metric_names),
|
| 92 |
+
timespan="PT1H",
|
| 93 |
+
interval="PT1M",
|
| 94 |
+
aggregation="Average"
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
results = []
|
| 98 |
+
for item in metrics_data.value:
|
| 99 |
+
for timeseries in item.timeseries:
|
| 100 |
+
for data in timeseries.data:
|
| 101 |
+
results.append({
|
| 102 |
+
"metric": item.name.value,
|
| 103 |
+
"timestamp": str(data.time_stamp),
|
| 104 |
+
"average": data.average
|
| 105 |
+
})
|
| 106 |
+
return results
|
| 107 |
+
except Exception as e:
|
| 108 |
+
return [{"error": str(e)}]
|
| 109 |
+
|
| 110 |
+
@mcp.tool()
|
| 111 |
+
def analyze_logs(workspace_id: str, query: str) -> List[Dict[str, Any]]:
|
| 112 |
+
"""
|
| 113 |
+
Execute KQL query on Log Analytics Workspace.
|
| 114 |
+
"""
|
| 115 |
+
log_usage("mcp-azure-sre", "analyze_logs")
|
| 116 |
+
try:
|
| 117 |
+
cred = get_credential()
|
| 118 |
+
client = LogsQueryClient(cred)
|
| 119 |
+
|
| 120 |
+
response = client.query_workspace(workspace_id, query, timespan="P1D")
|
| 121 |
+
|
| 122 |
+
if response.status == "Success":
|
| 123 |
+
# Convert table to list of dicts
|
| 124 |
+
results = []
|
| 125 |
+
for table in response.tables:
|
| 126 |
+
columns = table.columns
|
| 127 |
+
for row in table.rows:
|
| 128 |
+
results.append(dict(zip(columns, row)))
|
| 129 |
+
return results
|
| 130 |
+
else:
|
| 131 |
+
return [{"error": "Query failed"}]
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
return [{"error": str(e)}]
|
| 135 |
+
|
| 136 |
+
@mcp.tool()
|
| 137 |
+
def check_health(subscription_id: str, resource_group: str) -> Dict[str, str]:
|
| 138 |
+
"""
|
| 139 |
+
Perform a health check on key resources in a resource group.
|
| 140 |
+
Checks status of VMs.
|
| 141 |
+
"""
|
| 142 |
+
log_usage("mcp-azure-sre", "check_health")
|
| 143 |
+
try:
|
| 144 |
+
cred = get_credential()
|
| 145 |
+
compute_client = ComputeManagementClient(cred, subscription_id)
|
| 146 |
+
|
| 147 |
+
vms = compute_client.virtual_machines.list(resource_group)
|
| 148 |
+
health_status = {}
|
| 149 |
+
|
| 150 |
+
for vm in vms:
|
| 151 |
+
# Get instance view for power state
|
| 152 |
+
instance_view = compute_client.virtual_machines.instance_view(resource_group, vm.name)
|
| 153 |
+
statuses = [s.display_status for s in instance_view.statuses if s.code.startswith('PowerState')]
|
| 154 |
+
health_status[vm.name] = statuses[0] if statuses else "Unknown"
|
| 155 |
+
|
| 156 |
+
return health_status
|
| 157 |
+
except Exception as e:
|
| 158 |
+
return {"error": str(e)}
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
import os
|
| 162 |
+
if os.environ.get("MCP_TRANSPORT") == "sse":
|
| 163 |
+
import uvicorn
|
| 164 |
+
port = int(os.environ.get("PORT", 7860))
|
| 165 |
+
uvicorn.run(mcp.sse_app(), host="0.0.0.0", port=port)
|
| 166 |
+
else:
|
| 167 |
+
mcp.run()
|
src/mcp-github/Dockerfile
CHANGED
|
@@ -11,7 +11,7 @@ COPY pyproject.toml .
|
|
| 11 |
RUN pip install --no-cache-dir .
|
| 12 |
|
| 13 |
COPY src/mcp-github ./src/mcp-github
|
| 14 |
-
COPY src/
|
| 15 |
|
| 16 |
ENV PYTHONPATH=/app/src
|
| 17 |
|
|
|
|
| 11 |
RUN pip install --no-cache-dir .
|
| 12 |
|
| 13 |
COPY src/mcp-github ./src/mcp-github
|
| 14 |
+
COPY src/core ./src/core
|
| 15 |
|
| 16 |
ENV PYTHONPATH=/app/src
|
| 17 |
|
src/mcp-github/server.py
CHANGED
|
@@ -6,7 +6,7 @@ import sys
|
|
| 6 |
import os
|
| 7 |
from mcp.server.fastmcp import FastMCP
|
| 8 |
from typing import List, Dict, Any, Optional
|
| 9 |
-
from mcp_telemetry import log_usage
|
| 10 |
|
| 11 |
# Add src to pythonpath
|
| 12 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
| 6 |
import os
|
| 7 |
from mcp.server.fastmcp import FastMCP
|
| 8 |
from typing import List, Dict, Any, Optional
|
| 9 |
+
from core.mcp_telemetry import log_usage
|
| 10 |
|
| 11 |
# Add src to pythonpath
|
| 12 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
src/mcp-hub/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
src/mcp-hub/.vscode/extensions.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"recommendations": ["Vue.volar"]
|
| 3 |
+
}
|
src/mcp-hub/Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Build stage
|
| 2 |
+
FROM node:20-slim AS build-stage
|
| 3 |
+
WORKDIR /hub-build
|
| 4 |
+
# Copy the hub sources specifically for building
|
| 5 |
+
COPY src/mcp-hub ./
|
| 6 |
+
RUN npm install
|
| 7 |
+
RUN npm run build
|
| 8 |
+
|
| 9 |
+
# Production stage
|
| 10 |
+
FROM python:3.12-slim AS production-stage
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
|
| 13 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 14 |
+
git \
|
| 15 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
COPY pyproject.toml .
|
| 18 |
+
RUN pip install --no-cache-dir . fastapi uvicorn
|
| 19 |
+
|
| 20 |
+
COPY src/mcp-hub ./src/mcp-hub
|
| 21 |
+
COPY src/core ./src/core
|
| 22 |
+
# Copy all other MCP servers for discovery
|
| 23 |
+
COPY src/mcp-trader ./src/mcp-trader
|
| 24 |
+
COPY src/mcp-web ./src/mcp-web
|
| 25 |
+
COPY src/mcp-azure-sre ./src/mcp-azure-sre
|
| 26 |
+
COPY src/mcp-rag-secure ./src/mcp-rag-secure
|
| 27 |
+
COPY src/mcp-trading-research ./src/mcp-trading-research
|
| 28 |
+
COPY src/mcp-github ./src/mcp-github
|
| 29 |
+
COPY src/mcp-seo ./src/mcp-seo
|
| 30 |
+
COPY src/mcp-weather ./src/mcp-weather
|
| 31 |
+
|
| 32 |
+
COPY --from=build-stage /hub-build/dist ./src/mcp-hub/dist
|
| 33 |
+
|
| 34 |
+
ENV PYTHONPATH=/app/src
|
| 35 |
+
ENV PORT=7860
|
| 36 |
+
ENV MCP_IS_HUB=true
|
| 37 |
+
|
| 38 |
+
EXPOSE 7860
|
| 39 |
+
|
| 40 |
+
CMD ["python", "src/mcp-hub/api.py"]
|
src/mcp-hub/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
---
|
| 3 |
+
title: MCP HUB
|
| 4 |
+
emoji: 🚀
|
| 5 |
+
colorFrom: indigo
|
| 6 |
+
colorTo: purple
|
| 7 |
+
sdk: docker
|
| 8 |
+
pinned: true
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# MCP HUB Portal
|
| 12 |
+
|
| 13 |
+
A centralized discovery and monitoring dashboard for all available Model Context Protocol (MCP) servers.
|
| 14 |
+
|
| 15 |
+
## Telemetry & Metrics
|
| 16 |
+
|
| 17 |
+
The Hub acts as a centralized observability server for all MCP agents.
|
| 18 |
+
|
| 19 |
+
### Architecture
|
| 20 |
+
1. **Data Ingestion**:
|
| 21 |
+
- **Local Hub**: Writes directly to a lightweight SQLite database (`/app/data/logs.db`).
|
| 22 |
+
- **Remote Agents**: Send log events via HTTP POST to the Hub's `/api/telemetry` endpoint.
|
| 23 |
+
2. **Storage**: Data is stored in a relational `logs` table containing timestamp, server ID, and tool name.
|
| 24 |
+
3. **Visualization**: The dashboard polls the SQLite DB to render real-time "Usage Trends" charts.
|
| 25 |
+
|
| 26 |
+
### Future Proofing (Production Migration)
|
| 27 |
+
To scale beyond this monolithic architecture, the system is designed to be swappable with industry-standard tools:
|
| 28 |
+
1. **OpenTelemetry (OTel)**: Replace `src/core/mcp_telemetry.py` with the OTel Python SDK to export traces to Jaeger/Tempo.
|
| 29 |
+
2. **Prometheus**: Expose a `/metrics` endpoint (scraping `get_metrics()`) for Prometheus to ingest time-series data.
|
| 30 |
+
3. **Grafana**: Replace the built-in Vue.js charts with a hosted Grafana dashboard connected to the above data sources.
|
| 31 |
+
|
| 32 |
+
## Features
|
| 33 |
+
- **Server Discovery**: List of all 7 production-ready MCP servers.
|
| 34 |
+
- **Real-time Analytics**: Tracks hourly, weekly, and monthly usage trends.
|
| 35 |
+
- **Team Adoption**: Visual metrics to see how the team is utilizing different tools.
|
| 36 |
+
|
| 37 |
+
## Tech Stack
|
| 38 |
+
- **Frontend**: Vue.js 3 + Vite
|
| 39 |
+
- **Charts**: Plotly.js
|
| 40 |
+
- **Styling**: Vanilla CSS
|
| 41 |
+
- **Hosting**: Docker + Nginx
|
src/mcp-hub/api.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import json
|
| 4 |
+
import asyncio
|
| 5 |
+
from typing import List, Dict, Any, Optional
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from fastapi import FastAPI
|
| 8 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
+
from fastapi.staticfiles import StaticFiles
|
| 10 |
+
import uvicorn
|
| 11 |
+
from datetime import datetime, timedelta
|
| 12 |
+
|
| 13 |
+
# Add parent dir to path for imports
|
| 14 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
| 15 |
+
|
| 16 |
+
# Telemetry Import
|
| 17 |
+
try:
|
| 18 |
+
from core.mcp_telemetry import get_metrics, get_usage_history, get_system_metrics, log_usage, _get_conn, get_recent_logs
|
| 19 |
+
except ImportError:
|
| 20 |
+
# If standard import fails, try absolute path fallback
|
| 21 |
+
sys.path.append(str(Path(__file__).parent.parent.parent))
|
| 22 |
+
from src.core.mcp_telemetry import get_metrics, get_usage_history, get_system_metrics, log_usage, _get_conn, get_recent_logs
|
| 23 |
+
|
| 24 |
+
# Optional: HF Hub for status checks
|
| 25 |
+
try:
|
| 26 |
+
from huggingface_hub import HfApi
|
| 27 |
+
hf_api = HfApi()
|
| 28 |
+
except ImportError:
|
| 29 |
+
hf_api = None
|
| 30 |
+
|
| 31 |
+
from pydantic import BaseModel
|
| 32 |
+
|
| 33 |
+
class TelemetryEvent(BaseModel):
|
| 34 |
+
server: str
|
| 35 |
+
tool: str
|
| 36 |
+
timestamp: Optional[str] = None
|
| 37 |
+
|
| 38 |
+
app = FastAPI()
|
| 39 |
+
|
| 40 |
+
@app.post("/api/telemetry")
|
| 41 |
+
async def ingest_telemetry(event: TelemetryEvent):
|
| 42 |
+
"""Ingests telemetry from remote MCP agents."""
|
| 43 |
+
# We use the internal log_usage which handles DB writing
|
| 44 |
+
# We must ensure we are in Hub mode for this to work, which we are since this is api.py
|
| 45 |
+
# But wait, log_usage checks IS_HUB env var.
|
| 46 |
+
# To be safe, we will write directly or ensure env var is set in Dockerfile.
|
| 47 |
+
|
| 48 |
+
# Actually, simpler: we can just call the DB insert directly here to retrieve avoiding circular logic
|
| 49 |
+
# or just use log_usage if configured correctly.
|
| 50 |
+
|
| 51 |
+
# Let's import the specific DB function or use sqlite directly
|
| 52 |
+
from core.mcp_telemetry import _get_conn
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
ts = event.timestamp or datetime.now().isoformat()
|
| 56 |
+
with _get_conn() as conn:
|
| 57 |
+
conn.execute("INSERT INTO logs (timestamp, server, tool) VALUES (?, ?, ?)",
|
| 58 |
+
(ts, event.server, event.tool))
|
| 59 |
+
return {"status": "ok"}
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"Telemetry Ingest Failed: {e}")
|
| 62 |
+
return {"status": "error", "message": str(e)}
|
| 63 |
+
|
| 64 |
+
app.add_middleware(
|
| 65 |
+
CORSMiddleware,
|
| 66 |
+
allow_origins=["*"],
|
| 67 |
+
allow_methods=["*"],
|
| 68 |
+
allow_headers=["*"],
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
| 72 |
+
HF_USERNAME = os.environ.get("HF_USERNAME", "mishrabp")
|
| 73 |
+
|
| 74 |
+
KNOWN_SERVERS = [
|
| 75 |
+
{"id": "mcp-trader", "name": "MCP Trader", "description": "Quantitative trading strategies and market data analysis."},
|
| 76 |
+
{"id": "mcp-web", "name": "MCP Web", "description": "Web search, content extraction, and research tools."},
|
| 77 |
+
{"id": "mcp-azure-sre", "name": "MCP Azure SRE", "description": "Infrastructure management and monitoring for Azure."},
|
| 78 |
+
{"id": "mcp-rag-secure", "name": "MCP Secure RAG", "description": "Multi-tenant knowledge base with strict isolation."},
|
| 79 |
+
{"id": "mcp-trading-research", "name": "MCP Trading Research", "description": "Qualitative financial research and sentiment analysis."},
|
| 80 |
+
{"id": "mcp-github", "name": "MCP GitHub", "description": "GitHub repository management and automation."},
|
| 81 |
+
{"id": "mcp-seo", "name": "MCP SEO", "description": "Website auditing for SEO and accessibility."},
|
| 82 |
+
{"id": "mcp-weather", "name": "MCP Weather", "description": "Real-time weather forecast and location intelligence."}
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
async def get_hf_status(space_id: str) -> str:
|
| 86 |
+
"""Get status from Hugging Face Space with timeout."""
|
| 87 |
+
if not hf_api:
|
| 88 |
+
return "Unknown"
|
| 89 |
+
try:
|
| 90 |
+
# space_id is like "username/space-name"
|
| 91 |
+
repo_id = f"{HF_USERNAME}/{space_id}" if "/" not in space_id else space_id
|
| 92 |
+
# Use a thread pool or run_in_executor since get_space_runtime is blocking
|
| 93 |
+
loop = asyncio.get_event_loop()
|
| 94 |
+
runtime = await asyncio.wait_for(
|
| 95 |
+
loop.run_in_executor(None, lambda: hf_api.get_space_runtime(repo_id)),
|
| 96 |
+
timeout=5.0
|
| 97 |
+
)
|
| 98 |
+
return runtime.stage.capitalize()
|
| 99 |
+
except asyncio.TimeoutError:
|
| 100 |
+
print(f"Timeout checking status for {space_id}")
|
| 101 |
+
return "Timeout"
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f"Error checking status for {space_id}: {e}")
|
| 104 |
+
return "Offline"
|
| 105 |
+
|
| 106 |
+
@app.get("/api/servers/{server_id}")
|
| 107 |
+
async def get_server_detail(server_id: str):
|
| 108 |
+
"""Returns detailed documentation and tools for a specific server."""
|
| 109 |
+
# Log usage for trends
|
| 110 |
+
asyncio.create_task(asyncio.to_thread(log_usage, "MCP Hub", f"view_{server_id}"))
|
| 111 |
+
|
| 112 |
+
server_path = PROJECT_ROOT / "src" / server_id
|
| 113 |
+
readme_path = server_path / "README.md"
|
| 114 |
+
|
| 115 |
+
description = "No documentation found."
|
| 116 |
+
tools = []
|
| 117 |
+
|
| 118 |
+
if readme_path.exists():
|
| 119 |
+
content = readme_path.read_text()
|
| 120 |
+
# Parse description (text between # and ## Tools)
|
| 121 |
+
desc_match = content.split("## Tools")[0].split("#")
|
| 122 |
+
if len(desc_match) > 1:
|
| 123 |
+
description = desc_match[-1].split("---")[-1].strip()
|
| 124 |
+
|
| 125 |
+
# Parse tools
|
| 126 |
+
if "## Tools" in content:
|
| 127 |
+
tools_section = content.split("## Tools")[1].split("##")[0]
|
| 128 |
+
for line in tools_section.strip().split("\n"):
|
| 129 |
+
if line.strip().startswith("-"):
|
| 130 |
+
tools.append(line.strip("- ").strip())
|
| 131 |
+
|
| 132 |
+
# Apply strict capitalization
|
| 133 |
+
name = server_id.replace("-", " ").title()
|
| 134 |
+
for word in ["Mcp", "Sre", "Rag", "Seo", "mcp", "sre", "rag", "seo"]:
|
| 135 |
+
name = name.replace(word, word.upper())
|
| 136 |
+
description = description.replace(word, word.upper())
|
| 137 |
+
tools = [t.replace(word, word.upper()) for t in tools]
|
| 138 |
+
|
| 139 |
+
# Generate sample code
|
| 140 |
+
sample_code = f"""from openai_agents import Agent, Runner
|
| 141 |
+
from mcp_bridge import MCPBridge
|
| 142 |
+
|
| 143 |
+
# 1. Initialize Bridge
|
| 144 |
+
bridge = MCPBridge("https://{HF_USERNAME}-{server_id}.hf.space/sse")
|
| 145 |
+
|
| 146 |
+
# 2. Setup Agent with {name} Tools
|
| 147 |
+
agent = Agent(
|
| 148 |
+
name="{name} Expert",
|
| 149 |
+
instructions="You are an expert in {name}.",
|
| 150 |
+
functions=bridge.get_tools()
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
# 3. Execute
|
| 154 |
+
result = Runner.run(agent, "How can I use your tools?")
|
| 155 |
+
print(result.final_text)
|
| 156 |
+
"""
|
| 157 |
+
|
| 158 |
+
return {
|
| 159 |
+
"id": server_id,
|
| 160 |
+
"name": name,
|
| 161 |
+
"description": description,
|
| 162 |
+
"tools": tools,
|
| 163 |
+
"sample_code": sample_code,
|
| 164 |
+
"logs_url": f"https://huggingface.co/spaces/{HF_USERNAME}/{server_id}/logs"
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
@app.on_event("startup")
|
| 168 |
+
async def startup_event():
|
| 169 |
+
token = os.environ.get("HF_TOKEN")
|
| 170 |
+
if token:
|
| 171 |
+
print(f"HF_TOKEN found: {token[:4]}...{token[-4:]}")
|
| 172 |
+
else:
|
| 173 |
+
print("WARNING: HF_TOKEN not set! Live status checks will fail.")
|
| 174 |
+
|
| 175 |
+
@app.get("/api/servers/{server_id}/logs")
|
| 176 |
+
async def get_server_logs(server_id: str):
|
| 177 |
+
"""Fetches real-time runtime status and formats it as system logs."""
|
| 178 |
+
if not hf_api:
|
| 179 |
+
return {"logs": "[ERROR] HF API not initialized. Install huggingface_hub."}
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
repo_id = f"{HF_USERNAME}/{server_id}" if "/" not in server_id else server_id
|
| 183 |
+
|
| 184 |
+
# Debug print
|
| 185 |
+
print(f"Fetching logs for {repo_id}...")
|
| 186 |
+
|
| 187 |
+
loop = asyncio.get_event_loop()
|
| 188 |
+
runtime = await asyncio.wait_for(
|
| 189 |
+
loop.run_in_executor(None, lambda: hf_api.get_space_runtime(repo_id)),
|
| 190 |
+
timeout=10.0
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
# Format runtime info as logs
|
| 194 |
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 195 |
+
|
| 196 |
+
# Safely get hardware info
|
| 197 |
+
hardware_info = "UNKNOWN"
|
| 198 |
+
if hasattr(runtime, 'hardware') and runtime.hardware and hasattr(runtime.hardware, 'current'):
|
| 199 |
+
hardware_info = runtime.hardware.current
|
| 200 |
+
|
| 201 |
+
log_lines = [
|
| 202 |
+
f"[{ts}] SYSTEM_BOOT: Connected to MCP Stream",
|
| 203 |
+
f"[{ts}] TARGET_REPO: {repo_id}",
|
| 204 |
+
f"[{ts}] RUNTIME_STAGE: {runtime.stage.upper()}",
|
| 205 |
+
f"[{ts}] HARDWARE_SKU: {hardware_info}",
|
| 206 |
+
]
|
| 207 |
+
|
| 208 |
+
if hasattr(runtime, 'domains') and runtime.domains:
|
| 209 |
+
for d in runtime.domains:
|
| 210 |
+
log_lines.append(f"[{ts}] DOMAIN_BINDING: {d.domain} [{d.stage}]")
|
| 211 |
+
|
| 212 |
+
# Safely get replica info
|
| 213 |
+
replica_count = 1
|
| 214 |
+
if hasattr(runtime, 'replicas') and runtime.replicas and hasattr(runtime.replicas, 'current'):
|
| 215 |
+
replica_count = runtime.replicas.current
|
| 216 |
+
|
| 217 |
+
log_lines.append(f"[{ts}] REPLICA_COUNT: {replica_count}")
|
| 218 |
+
|
| 219 |
+
if runtime.stage == "RUNNING":
|
| 220 |
+
log_lines.append(f"[{ts}] STATUS_CHECK: HEALTHY")
|
| 221 |
+
log_lines.append(f"[{ts}] STREAM_GATEWAY: ACTIVE")
|
| 222 |
+
else:
|
| 223 |
+
log_lines.append(f"[{ts}] STATUS_CHECK: {runtime.stage}")
|
| 224 |
+
|
| 225 |
+
# --- REAL LOG INJECTION ---
|
| 226 |
+
# Get actual telemetry events from DB
|
| 227 |
+
try:
|
| 228 |
+
# server_id usually matches the DB server column (e.g. mcp-weather)
|
| 229 |
+
# but sometimes we might need mapping if ids differ. Assuming 1:1 for now.
|
| 230 |
+
start_marker = server_id.replace("mcp-", "").upper()
|
| 231 |
+
real_logs = get_recent_logs(server_id, limit=20)
|
| 232 |
+
|
| 233 |
+
if real_logs:
|
| 234 |
+
log_lines.append(f"[{ts}] --- RECENT ACTIVITY STREAM ---")
|
| 235 |
+
for l in real_logs:
|
| 236 |
+
# Parse ISO timestamp to look like log timestamp
|
| 237 |
+
try:
|
| 238 |
+
log_ts = datetime.fromisoformat(l["timestamp"]).strftime("%Y-%m-%d %H:%M:%S")
|
| 239 |
+
except:
|
| 240 |
+
log_ts = ts
|
| 241 |
+
log_lines.append(f"[{log_ts}] {start_marker}_TOOL: Executed '{l['tool']}'")
|
| 242 |
+
else:
|
| 243 |
+
log_lines.append(f"[{ts}] STREAM: No recent activity recorded.")
|
| 244 |
+
|
| 245 |
+
except Exception as ex:
|
| 246 |
+
log_lines.append(f"[{ts}] LOG_FETCH_ERROR: {str(ex)}")
|
| 247 |
+
|
| 248 |
+
return {"logs": "\n".join(log_lines)}
|
| 249 |
+
|
| 250 |
+
except Exception as e:
|
| 251 |
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 252 |
+
return {"logs": f"[{ts}] CONNECTION_ERROR: Failed to retrieve runtime status.\n[{ts}] DEBUG_TRACE: {str(e)}"}
|
| 253 |
+
|
| 254 |
+
@app.get("/api/servers")
|
| 255 |
+
async def list_servers():
|
| 256 |
+
"""Returns MCP servers with real metrics and HF status."""
|
| 257 |
+
metrics = get_metrics()
|
| 258 |
+
|
| 259 |
+
# 1. Discover local servers in src/
|
| 260 |
+
discovered = {}
|
| 261 |
+
if (PROJECT_ROOT / "src").exists():
|
| 262 |
+
for d in (PROJECT_ROOT / "src").iterdir():
|
| 263 |
+
if d.is_dir() and d.name.startswith("mcp-") and d.name != "mcp-hub":
|
| 264 |
+
readme_path = d / "README.md"
|
| 265 |
+
description = "MCP HUB Node"
|
| 266 |
+
if readme_path.exists():
|
| 267 |
+
lines = readme_path.read_text().split("\n")
|
| 268 |
+
# Try to find the first non-header line
|
| 269 |
+
for line in lines:
|
| 270 |
+
clean = line.strip()
|
| 271 |
+
if clean and not clean.startswith("#") and not clean.startswith("-"):
|
| 272 |
+
description = clean
|
| 273 |
+
break
|
| 274 |
+
|
| 275 |
+
name = d.name.replace("-", " ").title()
|
| 276 |
+
# Apply strict capitalization
|
| 277 |
+
for word in ["Mcp", "Sre", "Rag", "Seo", "mcp", "sre", "rag", "seo"]:
|
| 278 |
+
name = name.replace(word, word.upper())
|
| 279 |
+
|
| 280 |
+
description = description.replace("mcp", "MCP").replace("Mcp", "MCP").replace("sre", "SRE").replace("Sre", "SRE").replace("rag", "RAG").replace("Rag", "RAG").replace("seo", "SEO").replace("Seo", "SEO")
|
| 281 |
+
|
| 282 |
+
discovered[d.name] = {
|
| 283 |
+
"id": d.name,
|
| 284 |
+
"name": name,
|
| 285 |
+
"description": description
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
# 2. Merge with Known Servers (ensures we don't miss anything in Docker)
|
| 289 |
+
all_servers_map = {s["id"]: s for s in KNOWN_SERVERS}
|
| 290 |
+
all_servers_map.update(discovered) # Discovered overrides known if collision
|
| 291 |
+
|
| 292 |
+
servers_to_check = list(all_servers_map.values())
|
| 293 |
+
|
| 294 |
+
# 3. Check status in parallel
|
| 295 |
+
status_tasks = [get_hf_status(s["id"]) for s in servers_to_check]
|
| 296 |
+
statuses = await asyncio.gather(*status_tasks)
|
| 297 |
+
|
| 298 |
+
results = []
|
| 299 |
+
for idx, s in enumerate(servers_to_check):
|
| 300 |
+
server_metrics = metrics.get(s["id"], {"hourly": 0, "weekly": 0, "monthly": 0})
|
| 301 |
+
|
| 302 |
+
def fmt(n):
|
| 303 |
+
if n is None: return "0"
|
| 304 |
+
if n >= 1000: return f"{n/1000:.1f}k"
|
| 305 |
+
return str(n)
|
| 306 |
+
|
| 307 |
+
name = s["name"]
|
| 308 |
+
for word in ["Mcp", "Sre", "Rag", "Seo", "mcp", "sre", "rag", "seo"]:
|
| 309 |
+
name = name.replace(word, word.upper())
|
| 310 |
+
|
| 311 |
+
results.append({
|
| 312 |
+
**s,
|
| 313 |
+
"name": name,
|
| 314 |
+
"status": statuses[idx],
|
| 315 |
+
"metrics": {
|
| 316 |
+
"hourly": fmt(server_metrics.get("hourly", 0)),
|
| 317 |
+
"weekly": fmt(server_metrics.get("weekly", 0)),
|
| 318 |
+
"monthly": fmt(server_metrics.get("monthly", 0)),
|
| 319 |
+
"raw_hourly": server_metrics.get("hourly", 0),
|
| 320 |
+
"raw_weekly": server_metrics.get("weekly", 0),
|
| 321 |
+
"raw_monthly": server_metrics.get("monthly", 0)
|
| 322 |
+
}
|
| 323 |
+
})
|
| 324 |
+
|
| 325 |
+
return {
|
| 326 |
+
"servers": sorted(results, key=lambda x: x["name"]),
|
| 327 |
+
"system": get_system_metrics()
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
@app.get("/api/usage")
|
| 331 |
+
async def get_usage_trends(range: str = "24h"):
|
| 332 |
+
"""Returns real usage trends based on the requested time range."""
|
| 333 |
+
range_map = {
|
| 334 |
+
"1h": (1, 60), # 1 hour -> minutely (60 buckets)
|
| 335 |
+
"24h": (24, 24), # 24 hours -> hourly (24 buckets)
|
| 336 |
+
"7d": (168, 28), # 7 days -> 6-hourly (28 buckets)
|
| 337 |
+
"30d": (720, 30) # 30 days -> daily (30 buckets)
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
hours, intervals = range_map.get(range, (24, 24))
|
| 341 |
+
history = get_usage_history(range_hours=hours, intervals=intervals)
|
| 342 |
+
|
| 343 |
+
datasets = []
|
| 344 |
+
for server_id, counts in history["datasets"].items():
|
| 345 |
+
datasets.append({
|
| 346 |
+
"name": server_id.replace("mcp-", "").title(),
|
| 347 |
+
"data": counts
|
| 348 |
+
})
|
| 349 |
+
|
| 350 |
+
# If no data, return empty system load
|
| 351 |
+
if not datasets:
|
| 352 |
+
datasets.append({"name": "System", "data": [0] * intervals})
|
| 353 |
+
|
| 354 |
+
return {
|
| 355 |
+
"labels": history["labels"],
|
| 356 |
+
"datasets": datasets
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
from fastapi.responses import FileResponse
|
| 360 |
+
|
| 361 |
+
# Mount static files
|
| 362 |
+
static_path = Path(__file__).parent / "dist"
|
| 363 |
+
|
| 364 |
+
if static_path.exists():
|
| 365 |
+
# Mount assets folder specifically
|
| 366 |
+
app.mount("/assets", StaticFiles(directory=str(static_path / "assets")), name="assets")
|
| 367 |
+
|
| 368 |
+
@app.get("/{full_path:path}")
|
| 369 |
+
async def serve_spa(full_path: str):
|
| 370 |
+
# Check if the requested path exists as a file in dist (e.g., vite.svg)
|
| 371 |
+
file_path = static_path / full_path
|
| 372 |
+
if file_path.is_file():
|
| 373 |
+
return FileResponse(file_path)
|
| 374 |
+
|
| 375 |
+
# Otherwise, serve index.html for SPA routing
|
| 376 |
+
index_path = static_path / "index.html"
|
| 377 |
+
if index_path.exists():
|
| 378 |
+
return FileResponse(index_path)
|
| 379 |
+
|
| 380 |
+
return {"error": "Frontend not built. Run 'npm run build' in src/mcp-hub"}
|
| 381 |
+
|
| 382 |
+
if __name__ == "__main__":
|
| 383 |
+
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
|
src/mcp-hub/index.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8" />
|
| 6 |
+
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
| 7 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 8 |
+
<title>MCP HUB</title>
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
| 12 |
+
</head>
|
| 13 |
+
|
| 14 |
+
<body>
|
| 15 |
+
<div id="app"></div>
|
| 16 |
+
<script type="module" src="/src/main.js"></script>
|
| 17 |
+
</body>
|
| 18 |
+
|
| 19 |
+
</html>
|
src/mcp-hub/nginx.conf
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
server {
|
| 3 |
+
listen 7860;
|
| 4 |
+
server_name localhost;
|
| 5 |
+
|
| 6 |
+
location / {
|
| 7 |
+
root /usr/share/nginx/html;
|
| 8 |
+
index index.html;
|
| 9 |
+
try_files $uri $uri/ /index.html;
|
| 10 |
+
}
|
| 11 |
+
}
|
src/mcp-hub/package-lock.json
ADDED
|
@@ -0,0 +1,1341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "mcp-hub",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "mcp-hub",
|
| 9 |
+
"version": "0.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"plotly.js-dist-min": "^3.3.1",
|
| 12 |
+
"vue": "^3.5.24"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"@vitejs/plugin-vue": "^6.0.1",
|
| 16 |
+
"vite": "^7.2.4"
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
"node_modules/@babel/helper-string-parser": {
|
| 20 |
+
"version": "7.27.1",
|
| 21 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 22 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 23 |
+
"license": "MIT",
|
| 24 |
+
"engines": {
|
| 25 |
+
"node": ">=6.9.0"
|
| 26 |
+
}
|
| 27 |
+
},
|
| 28 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 29 |
+
"version": "7.28.5",
|
| 30 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 31 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 32 |
+
"license": "MIT",
|
| 33 |
+
"engines": {
|
| 34 |
+
"node": ">=6.9.0"
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
"node_modules/@babel/parser": {
|
| 38 |
+
"version": "7.29.0",
|
| 39 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
|
| 40 |
+
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
| 41 |
+
"license": "MIT",
|
| 42 |
+
"dependencies": {
|
| 43 |
+
"@babel/types": "^7.29.0"
|
| 44 |
+
},
|
| 45 |
+
"bin": {
|
| 46 |
+
"parser": "bin/babel-parser.js"
|
| 47 |
+
},
|
| 48 |
+
"engines": {
|
| 49 |
+
"node": ">=6.0.0"
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
"node_modules/@babel/types": {
|
| 53 |
+
"version": "7.29.0",
|
| 54 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
| 55 |
+
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 56 |
+
"license": "MIT",
|
| 57 |
+
"dependencies": {
|
| 58 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 59 |
+
"@babel/helper-validator-identifier": "^7.28.5"
|
| 60 |
+
},
|
| 61 |
+
"engines": {
|
| 62 |
+
"node": ">=6.9.0"
|
| 63 |
+
}
|
| 64 |
+
},
|
| 65 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 66 |
+
"version": "0.27.3",
|
| 67 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
| 68 |
+
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
| 69 |
+
"cpu": [
|
| 70 |
+
"ppc64"
|
| 71 |
+
],
|
| 72 |
+
"dev": true,
|
| 73 |
+
"license": "MIT",
|
| 74 |
+
"optional": true,
|
| 75 |
+
"os": [
|
| 76 |
+
"aix"
|
| 77 |
+
],
|
| 78 |
+
"engines": {
|
| 79 |
+
"node": ">=18"
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
"node_modules/@esbuild/android-arm": {
|
| 83 |
+
"version": "0.27.3",
|
| 84 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
| 85 |
+
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
| 86 |
+
"cpu": [
|
| 87 |
+
"arm"
|
| 88 |
+
],
|
| 89 |
+
"dev": true,
|
| 90 |
+
"license": "MIT",
|
| 91 |
+
"optional": true,
|
| 92 |
+
"os": [
|
| 93 |
+
"android"
|
| 94 |
+
],
|
| 95 |
+
"engines": {
|
| 96 |
+
"node": ">=18"
|
| 97 |
+
}
|
| 98 |
+
},
|
| 99 |
+
"node_modules/@esbuild/android-arm64": {
|
| 100 |
+
"version": "0.27.3",
|
| 101 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
| 102 |
+
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
| 103 |
+
"cpu": [
|
| 104 |
+
"arm64"
|
| 105 |
+
],
|
| 106 |
+
"dev": true,
|
| 107 |
+
"license": "MIT",
|
| 108 |
+
"optional": true,
|
| 109 |
+
"os": [
|
| 110 |
+
"android"
|
| 111 |
+
],
|
| 112 |
+
"engines": {
|
| 113 |
+
"node": ">=18"
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
"node_modules/@esbuild/android-x64": {
|
| 117 |
+
"version": "0.27.3",
|
| 118 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
| 119 |
+
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
| 120 |
+
"cpu": [
|
| 121 |
+
"x64"
|
| 122 |
+
],
|
| 123 |
+
"dev": true,
|
| 124 |
+
"license": "MIT",
|
| 125 |
+
"optional": true,
|
| 126 |
+
"os": [
|
| 127 |
+
"android"
|
| 128 |
+
],
|
| 129 |
+
"engines": {
|
| 130 |
+
"node": ">=18"
|
| 131 |
+
}
|
| 132 |
+
},
|
| 133 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 134 |
+
"version": "0.27.3",
|
| 135 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
| 136 |
+
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
| 137 |
+
"cpu": [
|
| 138 |
+
"arm64"
|
| 139 |
+
],
|
| 140 |
+
"dev": true,
|
| 141 |
+
"license": "MIT",
|
| 142 |
+
"optional": true,
|
| 143 |
+
"os": [
|
| 144 |
+
"darwin"
|
| 145 |
+
],
|
| 146 |
+
"engines": {
|
| 147 |
+
"node": ">=18"
|
| 148 |
+
}
|
| 149 |
+
},
|
| 150 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 151 |
+
"version": "0.27.3",
|
| 152 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
| 153 |
+
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
| 154 |
+
"cpu": [
|
| 155 |
+
"x64"
|
| 156 |
+
],
|
| 157 |
+
"dev": true,
|
| 158 |
+
"license": "MIT",
|
| 159 |
+
"optional": true,
|
| 160 |
+
"os": [
|
| 161 |
+
"darwin"
|
| 162 |
+
],
|
| 163 |
+
"engines": {
|
| 164 |
+
"node": ">=18"
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 168 |
+
"version": "0.27.3",
|
| 169 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
| 170 |
+
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
| 171 |
+
"cpu": [
|
| 172 |
+
"arm64"
|
| 173 |
+
],
|
| 174 |
+
"dev": true,
|
| 175 |
+
"license": "MIT",
|
| 176 |
+
"optional": true,
|
| 177 |
+
"os": [
|
| 178 |
+
"freebsd"
|
| 179 |
+
],
|
| 180 |
+
"engines": {
|
| 181 |
+
"node": ">=18"
|
| 182 |
+
}
|
| 183 |
+
},
|
| 184 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 185 |
+
"version": "0.27.3",
|
| 186 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
| 187 |
+
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
| 188 |
+
"cpu": [
|
| 189 |
+
"x64"
|
| 190 |
+
],
|
| 191 |
+
"dev": true,
|
| 192 |
+
"license": "MIT",
|
| 193 |
+
"optional": true,
|
| 194 |
+
"os": [
|
| 195 |
+
"freebsd"
|
| 196 |
+
],
|
| 197 |
+
"engines": {
|
| 198 |
+
"node": ">=18"
|
| 199 |
+
}
|
| 200 |
+
},
|
| 201 |
+
"node_modules/@esbuild/linux-arm": {
|
| 202 |
+
"version": "0.27.3",
|
| 203 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
| 204 |
+
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
| 205 |
+
"cpu": [
|
| 206 |
+
"arm"
|
| 207 |
+
],
|
| 208 |
+
"dev": true,
|
| 209 |
+
"license": "MIT",
|
| 210 |
+
"optional": true,
|
| 211 |
+
"os": [
|
| 212 |
+
"linux"
|
| 213 |
+
],
|
| 214 |
+
"engines": {
|
| 215 |
+
"node": ">=18"
|
| 216 |
+
}
|
| 217 |
+
},
|
| 218 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 219 |
+
"version": "0.27.3",
|
| 220 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
| 221 |
+
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
| 222 |
+
"cpu": [
|
| 223 |
+
"arm64"
|
| 224 |
+
],
|
| 225 |
+
"dev": true,
|
| 226 |
+
"license": "MIT",
|
| 227 |
+
"optional": true,
|
| 228 |
+
"os": [
|
| 229 |
+
"linux"
|
| 230 |
+
],
|
| 231 |
+
"engines": {
|
| 232 |
+
"node": ">=18"
|
| 233 |
+
}
|
| 234 |
+
},
|
| 235 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 236 |
+
"version": "0.27.3",
|
| 237 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
| 238 |
+
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
| 239 |
+
"cpu": [
|
| 240 |
+
"ia32"
|
| 241 |
+
],
|
| 242 |
+
"dev": true,
|
| 243 |
+
"license": "MIT",
|
| 244 |
+
"optional": true,
|
| 245 |
+
"os": [
|
| 246 |
+
"linux"
|
| 247 |
+
],
|
| 248 |
+
"engines": {
|
| 249 |
+
"node": ">=18"
|
| 250 |
+
}
|
| 251 |
+
},
|
| 252 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 253 |
+
"version": "0.27.3",
|
| 254 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
| 255 |
+
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
| 256 |
+
"cpu": [
|
| 257 |
+
"loong64"
|
| 258 |
+
],
|
| 259 |
+
"dev": true,
|
| 260 |
+
"license": "MIT",
|
| 261 |
+
"optional": true,
|
| 262 |
+
"os": [
|
| 263 |
+
"linux"
|
| 264 |
+
],
|
| 265 |
+
"engines": {
|
| 266 |
+
"node": ">=18"
|
| 267 |
+
}
|
| 268 |
+
},
|
| 269 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 270 |
+
"version": "0.27.3",
|
| 271 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
| 272 |
+
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
| 273 |
+
"cpu": [
|
| 274 |
+
"mips64el"
|
| 275 |
+
],
|
| 276 |
+
"dev": true,
|
| 277 |
+
"license": "MIT",
|
| 278 |
+
"optional": true,
|
| 279 |
+
"os": [
|
| 280 |
+
"linux"
|
| 281 |
+
],
|
| 282 |
+
"engines": {
|
| 283 |
+
"node": ">=18"
|
| 284 |
+
}
|
| 285 |
+
},
|
| 286 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 287 |
+
"version": "0.27.3",
|
| 288 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
| 289 |
+
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
| 290 |
+
"cpu": [
|
| 291 |
+
"ppc64"
|
| 292 |
+
],
|
| 293 |
+
"dev": true,
|
| 294 |
+
"license": "MIT",
|
| 295 |
+
"optional": true,
|
| 296 |
+
"os": [
|
| 297 |
+
"linux"
|
| 298 |
+
],
|
| 299 |
+
"engines": {
|
| 300 |
+
"node": ">=18"
|
| 301 |
+
}
|
| 302 |
+
},
|
| 303 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 304 |
+
"version": "0.27.3",
|
| 305 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
| 306 |
+
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
| 307 |
+
"cpu": [
|
| 308 |
+
"riscv64"
|
| 309 |
+
],
|
| 310 |
+
"dev": true,
|
| 311 |
+
"license": "MIT",
|
| 312 |
+
"optional": true,
|
| 313 |
+
"os": [
|
| 314 |
+
"linux"
|
| 315 |
+
],
|
| 316 |
+
"engines": {
|
| 317 |
+
"node": ">=18"
|
| 318 |
+
}
|
| 319 |
+
},
|
| 320 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 321 |
+
"version": "0.27.3",
|
| 322 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
| 323 |
+
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
| 324 |
+
"cpu": [
|
| 325 |
+
"s390x"
|
| 326 |
+
],
|
| 327 |
+
"dev": true,
|
| 328 |
+
"license": "MIT",
|
| 329 |
+
"optional": true,
|
| 330 |
+
"os": [
|
| 331 |
+
"linux"
|
| 332 |
+
],
|
| 333 |
+
"engines": {
|
| 334 |
+
"node": ">=18"
|
| 335 |
+
}
|
| 336 |
+
},
|
| 337 |
+
"node_modules/@esbuild/linux-x64": {
|
| 338 |
+
"version": "0.27.3",
|
| 339 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
| 340 |
+
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
| 341 |
+
"cpu": [
|
| 342 |
+
"x64"
|
| 343 |
+
],
|
| 344 |
+
"dev": true,
|
| 345 |
+
"license": "MIT",
|
| 346 |
+
"optional": true,
|
| 347 |
+
"os": [
|
| 348 |
+
"linux"
|
| 349 |
+
],
|
| 350 |
+
"engines": {
|
| 351 |
+
"node": ">=18"
|
| 352 |
+
}
|
| 353 |
+
},
|
| 354 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 355 |
+
"version": "0.27.3",
|
| 356 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
| 357 |
+
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
| 358 |
+
"cpu": [
|
| 359 |
+
"arm64"
|
| 360 |
+
],
|
| 361 |
+
"dev": true,
|
| 362 |
+
"license": "MIT",
|
| 363 |
+
"optional": true,
|
| 364 |
+
"os": [
|
| 365 |
+
"netbsd"
|
| 366 |
+
],
|
| 367 |
+
"engines": {
|
| 368 |
+
"node": ">=18"
|
| 369 |
+
}
|
| 370 |
+
},
|
| 371 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 372 |
+
"version": "0.27.3",
|
| 373 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
| 374 |
+
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
| 375 |
+
"cpu": [
|
| 376 |
+
"x64"
|
| 377 |
+
],
|
| 378 |
+
"dev": true,
|
| 379 |
+
"license": "MIT",
|
| 380 |
+
"optional": true,
|
| 381 |
+
"os": [
|
| 382 |
+
"netbsd"
|
| 383 |
+
],
|
| 384 |
+
"engines": {
|
| 385 |
+
"node": ">=18"
|
| 386 |
+
}
|
| 387 |
+
},
|
| 388 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 389 |
+
"version": "0.27.3",
|
| 390 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
| 391 |
+
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
| 392 |
+
"cpu": [
|
| 393 |
+
"arm64"
|
| 394 |
+
],
|
| 395 |
+
"dev": true,
|
| 396 |
+
"license": "MIT",
|
| 397 |
+
"optional": true,
|
| 398 |
+
"os": [
|
| 399 |
+
"openbsd"
|
| 400 |
+
],
|
| 401 |
+
"engines": {
|
| 402 |
+
"node": ">=18"
|
| 403 |
+
}
|
| 404 |
+
},
|
| 405 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 406 |
+
"version": "0.27.3",
|
| 407 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
| 408 |
+
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
| 409 |
+
"cpu": [
|
| 410 |
+
"x64"
|
| 411 |
+
],
|
| 412 |
+
"dev": true,
|
| 413 |
+
"license": "MIT",
|
| 414 |
+
"optional": true,
|
| 415 |
+
"os": [
|
| 416 |
+
"openbsd"
|
| 417 |
+
],
|
| 418 |
+
"engines": {
|
| 419 |
+
"node": ">=18"
|
| 420 |
+
}
|
| 421 |
+
},
|
| 422 |
+
"node_modules/@esbuild/openharmony-arm64": {
|
| 423 |
+
"version": "0.27.3",
|
| 424 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
| 425 |
+
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
| 426 |
+
"cpu": [
|
| 427 |
+
"arm64"
|
| 428 |
+
],
|
| 429 |
+
"dev": true,
|
| 430 |
+
"license": "MIT",
|
| 431 |
+
"optional": true,
|
| 432 |
+
"os": [
|
| 433 |
+
"openharmony"
|
| 434 |
+
],
|
| 435 |
+
"engines": {
|
| 436 |
+
"node": ">=18"
|
| 437 |
+
}
|
| 438 |
+
},
|
| 439 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 440 |
+
"version": "0.27.3",
|
| 441 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
| 442 |
+
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
| 443 |
+
"cpu": [
|
| 444 |
+
"x64"
|
| 445 |
+
],
|
| 446 |
+
"dev": true,
|
| 447 |
+
"license": "MIT",
|
| 448 |
+
"optional": true,
|
| 449 |
+
"os": [
|
| 450 |
+
"sunos"
|
| 451 |
+
],
|
| 452 |
+
"engines": {
|
| 453 |
+
"node": ">=18"
|
| 454 |
+
}
|
| 455 |
+
},
|
| 456 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 457 |
+
"version": "0.27.3",
|
| 458 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
| 459 |
+
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
| 460 |
+
"cpu": [
|
| 461 |
+
"arm64"
|
| 462 |
+
],
|
| 463 |
+
"dev": true,
|
| 464 |
+
"license": "MIT",
|
| 465 |
+
"optional": true,
|
| 466 |
+
"os": [
|
| 467 |
+
"win32"
|
| 468 |
+
],
|
| 469 |
+
"engines": {
|
| 470 |
+
"node": ">=18"
|
| 471 |
+
}
|
| 472 |
+
},
|
| 473 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 474 |
+
"version": "0.27.3",
|
| 475 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
| 476 |
+
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
| 477 |
+
"cpu": [
|
| 478 |
+
"ia32"
|
| 479 |
+
],
|
| 480 |
+
"dev": true,
|
| 481 |
+
"license": "MIT",
|
| 482 |
+
"optional": true,
|
| 483 |
+
"os": [
|
| 484 |
+
"win32"
|
| 485 |
+
],
|
| 486 |
+
"engines": {
|
| 487 |
+
"node": ">=18"
|
| 488 |
+
}
|
| 489 |
+
},
|
| 490 |
+
"node_modules/@esbuild/win32-x64": {
|
| 491 |
+
"version": "0.27.3",
|
| 492 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
| 493 |
+
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
| 494 |
+
"cpu": [
|
| 495 |
+
"x64"
|
| 496 |
+
],
|
| 497 |
+
"dev": true,
|
| 498 |
+
"license": "MIT",
|
| 499 |
+
"optional": true,
|
| 500 |
+
"os": [
|
| 501 |
+
"win32"
|
| 502 |
+
],
|
| 503 |
+
"engines": {
|
| 504 |
+
"node": ">=18"
|
| 505 |
+
}
|
| 506 |
+
},
|
| 507 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 508 |
+
"version": "1.5.5",
|
| 509 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 510 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 511 |
+
"license": "MIT"
|
| 512 |
+
},
|
| 513 |
+
"node_modules/@rolldown/pluginutils": {
|
| 514 |
+
"version": "1.0.0-rc.2",
|
| 515 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
|
| 516 |
+
"integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
|
| 517 |
+
"dev": true,
|
| 518 |
+
"license": "MIT"
|
| 519 |
+
},
|
| 520 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 521 |
+
"version": "4.57.1",
|
| 522 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
| 523 |
+
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
|
| 524 |
+
"cpu": [
|
| 525 |
+
"arm"
|
| 526 |
+
],
|
| 527 |
+
"dev": true,
|
| 528 |
+
"license": "MIT",
|
| 529 |
+
"optional": true,
|
| 530 |
+
"os": [
|
| 531 |
+
"android"
|
| 532 |
+
]
|
| 533 |
+
},
|
| 534 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 535 |
+
"version": "4.57.1",
|
| 536 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
|
| 537 |
+
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
|
| 538 |
+
"cpu": [
|
| 539 |
+
"arm64"
|
| 540 |
+
],
|
| 541 |
+
"dev": true,
|
| 542 |
+
"license": "MIT",
|
| 543 |
+
"optional": true,
|
| 544 |
+
"os": [
|
| 545 |
+
"android"
|
| 546 |
+
]
|
| 547 |
+
},
|
| 548 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 549 |
+
"version": "4.57.1",
|
| 550 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
| 551 |
+
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
| 552 |
+
"cpu": [
|
| 553 |
+
"arm64"
|
| 554 |
+
],
|
| 555 |
+
"dev": true,
|
| 556 |
+
"license": "MIT",
|
| 557 |
+
"optional": true,
|
| 558 |
+
"os": [
|
| 559 |
+
"darwin"
|
| 560 |
+
]
|
| 561 |
+
},
|
| 562 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 563 |
+
"version": "4.57.1",
|
| 564 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
| 565 |
+
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
| 566 |
+
"cpu": [
|
| 567 |
+
"x64"
|
| 568 |
+
],
|
| 569 |
+
"dev": true,
|
| 570 |
+
"license": "MIT",
|
| 571 |
+
"optional": true,
|
| 572 |
+
"os": [
|
| 573 |
+
"darwin"
|
| 574 |
+
]
|
| 575 |
+
},
|
| 576 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 577 |
+
"version": "4.57.1",
|
| 578 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
|
| 579 |
+
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
|
| 580 |
+
"cpu": [
|
| 581 |
+
"arm64"
|
| 582 |
+
],
|
| 583 |
+
"dev": true,
|
| 584 |
+
"license": "MIT",
|
| 585 |
+
"optional": true,
|
| 586 |
+
"os": [
|
| 587 |
+
"freebsd"
|
| 588 |
+
]
|
| 589 |
+
},
|
| 590 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 591 |
+
"version": "4.57.1",
|
| 592 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
|
| 593 |
+
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
|
| 594 |
+
"cpu": [
|
| 595 |
+
"x64"
|
| 596 |
+
],
|
| 597 |
+
"dev": true,
|
| 598 |
+
"license": "MIT",
|
| 599 |
+
"optional": true,
|
| 600 |
+
"os": [
|
| 601 |
+
"freebsd"
|
| 602 |
+
]
|
| 603 |
+
},
|
| 604 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 605 |
+
"version": "4.57.1",
|
| 606 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
|
| 607 |
+
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
|
| 608 |
+
"cpu": [
|
| 609 |
+
"arm"
|
| 610 |
+
],
|
| 611 |
+
"dev": true,
|
| 612 |
+
"license": "MIT",
|
| 613 |
+
"optional": true,
|
| 614 |
+
"os": [
|
| 615 |
+
"linux"
|
| 616 |
+
]
|
| 617 |
+
},
|
| 618 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 619 |
+
"version": "4.57.1",
|
| 620 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
|
| 621 |
+
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
|
| 622 |
+
"cpu": [
|
| 623 |
+
"arm"
|
| 624 |
+
],
|
| 625 |
+
"dev": true,
|
| 626 |
+
"license": "MIT",
|
| 627 |
+
"optional": true,
|
| 628 |
+
"os": [
|
| 629 |
+
"linux"
|
| 630 |
+
]
|
| 631 |
+
},
|
| 632 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 633 |
+
"version": "4.57.1",
|
| 634 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
| 635 |
+
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
| 636 |
+
"cpu": [
|
| 637 |
+
"arm64"
|
| 638 |
+
],
|
| 639 |
+
"dev": true,
|
| 640 |
+
"license": "MIT",
|
| 641 |
+
"optional": true,
|
| 642 |
+
"os": [
|
| 643 |
+
"linux"
|
| 644 |
+
]
|
| 645 |
+
},
|
| 646 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 647 |
+
"version": "4.57.1",
|
| 648 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
|
| 649 |
+
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
|
| 650 |
+
"cpu": [
|
| 651 |
+
"arm64"
|
| 652 |
+
],
|
| 653 |
+
"dev": true,
|
| 654 |
+
"license": "MIT",
|
| 655 |
+
"optional": true,
|
| 656 |
+
"os": [
|
| 657 |
+
"linux"
|
| 658 |
+
]
|
| 659 |
+
},
|
| 660 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 661 |
+
"version": "4.57.1",
|
| 662 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
|
| 663 |
+
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
|
| 664 |
+
"cpu": [
|
| 665 |
+
"loong64"
|
| 666 |
+
],
|
| 667 |
+
"dev": true,
|
| 668 |
+
"license": "MIT",
|
| 669 |
+
"optional": true,
|
| 670 |
+
"os": [
|
| 671 |
+
"linux"
|
| 672 |
+
]
|
| 673 |
+
},
|
| 674 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 675 |
+
"version": "4.57.1",
|
| 676 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
|
| 677 |
+
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
|
| 678 |
+
"cpu": [
|
| 679 |
+
"loong64"
|
| 680 |
+
],
|
| 681 |
+
"dev": true,
|
| 682 |
+
"license": "MIT",
|
| 683 |
+
"optional": true,
|
| 684 |
+
"os": [
|
| 685 |
+
"linux"
|
| 686 |
+
]
|
| 687 |
+
},
|
| 688 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 689 |
+
"version": "4.57.1",
|
| 690 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
|
| 691 |
+
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
|
| 692 |
+
"cpu": [
|
| 693 |
+
"ppc64"
|
| 694 |
+
],
|
| 695 |
+
"dev": true,
|
| 696 |
+
"license": "MIT",
|
| 697 |
+
"optional": true,
|
| 698 |
+
"os": [
|
| 699 |
+
"linux"
|
| 700 |
+
]
|
| 701 |
+
},
|
| 702 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 703 |
+
"version": "4.57.1",
|
| 704 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
|
| 705 |
+
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
|
| 706 |
+
"cpu": [
|
| 707 |
+
"ppc64"
|
| 708 |
+
],
|
| 709 |
+
"dev": true,
|
| 710 |
+
"license": "MIT",
|
| 711 |
+
"optional": true,
|
| 712 |
+
"os": [
|
| 713 |
+
"linux"
|
| 714 |
+
]
|
| 715 |
+
},
|
| 716 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 717 |
+
"version": "4.57.1",
|
| 718 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
|
| 719 |
+
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
|
| 720 |
+
"cpu": [
|
| 721 |
+
"riscv64"
|
| 722 |
+
],
|
| 723 |
+
"dev": true,
|
| 724 |
+
"license": "MIT",
|
| 725 |
+
"optional": true,
|
| 726 |
+
"os": [
|
| 727 |
+
"linux"
|
| 728 |
+
]
|
| 729 |
+
},
|
| 730 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 731 |
+
"version": "4.57.1",
|
| 732 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
|
| 733 |
+
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
|
| 734 |
+
"cpu": [
|
| 735 |
+
"riscv64"
|
| 736 |
+
],
|
| 737 |
+
"dev": true,
|
| 738 |
+
"license": "MIT",
|
| 739 |
+
"optional": true,
|
| 740 |
+
"os": [
|
| 741 |
+
"linux"
|
| 742 |
+
]
|
| 743 |
+
},
|
| 744 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 745 |
+
"version": "4.57.1",
|
| 746 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
|
| 747 |
+
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
|
| 748 |
+
"cpu": [
|
| 749 |
+
"s390x"
|
| 750 |
+
],
|
| 751 |
+
"dev": true,
|
| 752 |
+
"license": "MIT",
|
| 753 |
+
"optional": true,
|
| 754 |
+
"os": [
|
| 755 |
+
"linux"
|
| 756 |
+
]
|
| 757 |
+
},
|
| 758 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 759 |
+
"version": "4.57.1",
|
| 760 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
|
| 761 |
+
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
|
| 762 |
+
"cpu": [
|
| 763 |
+
"x64"
|
| 764 |
+
],
|
| 765 |
+
"dev": true,
|
| 766 |
+
"license": "MIT",
|
| 767 |
+
"optional": true,
|
| 768 |
+
"os": [
|
| 769 |
+
"linux"
|
| 770 |
+
]
|
| 771 |
+
},
|
| 772 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 773 |
+
"version": "4.57.1",
|
| 774 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
|
| 775 |
+
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
|
| 776 |
+
"cpu": [
|
| 777 |
+
"x64"
|
| 778 |
+
],
|
| 779 |
+
"dev": true,
|
| 780 |
+
"license": "MIT",
|
| 781 |
+
"optional": true,
|
| 782 |
+
"os": [
|
| 783 |
+
"linux"
|
| 784 |
+
]
|
| 785 |
+
},
|
| 786 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 787 |
+
"version": "4.57.1",
|
| 788 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
|
| 789 |
+
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
|
| 790 |
+
"cpu": [
|
| 791 |
+
"x64"
|
| 792 |
+
],
|
| 793 |
+
"dev": true,
|
| 794 |
+
"license": "MIT",
|
| 795 |
+
"optional": true,
|
| 796 |
+
"os": [
|
| 797 |
+
"openbsd"
|
| 798 |
+
]
|
| 799 |
+
},
|
| 800 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 801 |
+
"version": "4.57.1",
|
| 802 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
|
| 803 |
+
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
|
| 804 |
+
"cpu": [
|
| 805 |
+
"arm64"
|
| 806 |
+
],
|
| 807 |
+
"dev": true,
|
| 808 |
+
"license": "MIT",
|
| 809 |
+
"optional": true,
|
| 810 |
+
"os": [
|
| 811 |
+
"openharmony"
|
| 812 |
+
]
|
| 813 |
+
},
|
| 814 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 815 |
+
"version": "4.57.1",
|
| 816 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
| 817 |
+
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
| 818 |
+
"cpu": [
|
| 819 |
+
"arm64"
|
| 820 |
+
],
|
| 821 |
+
"dev": true,
|
| 822 |
+
"license": "MIT",
|
| 823 |
+
"optional": true,
|
| 824 |
+
"os": [
|
| 825 |
+
"win32"
|
| 826 |
+
]
|
| 827 |
+
},
|
| 828 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 829 |
+
"version": "4.57.1",
|
| 830 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
|
| 831 |
+
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
|
| 832 |
+
"cpu": [
|
| 833 |
+
"ia32"
|
| 834 |
+
],
|
| 835 |
+
"dev": true,
|
| 836 |
+
"license": "MIT",
|
| 837 |
+
"optional": true,
|
| 838 |
+
"os": [
|
| 839 |
+
"win32"
|
| 840 |
+
]
|
| 841 |
+
},
|
| 842 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 843 |
+
"version": "4.57.1",
|
| 844 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
|
| 845 |
+
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
|
| 846 |
+
"cpu": [
|
| 847 |
+
"x64"
|
| 848 |
+
],
|
| 849 |
+
"dev": true,
|
| 850 |
+
"license": "MIT",
|
| 851 |
+
"optional": true,
|
| 852 |
+
"os": [
|
| 853 |
+
"win32"
|
| 854 |
+
]
|
| 855 |
+
},
|
| 856 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 857 |
+
"version": "4.57.1",
|
| 858 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
| 859 |
+
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
| 860 |
+
"cpu": [
|
| 861 |
+
"x64"
|
| 862 |
+
],
|
| 863 |
+
"dev": true,
|
| 864 |
+
"license": "MIT",
|
| 865 |
+
"optional": true,
|
| 866 |
+
"os": [
|
| 867 |
+
"win32"
|
| 868 |
+
]
|
| 869 |
+
},
|
| 870 |
+
"node_modules/@types/estree": {
|
| 871 |
+
"version": "1.0.8",
|
| 872 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 873 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 874 |
+
"dev": true,
|
| 875 |
+
"license": "MIT"
|
| 876 |
+
},
|
| 877 |
+
"node_modules/@vitejs/plugin-vue": {
|
| 878 |
+
"version": "6.0.4",
|
| 879 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz",
|
| 880 |
+
"integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==",
|
| 881 |
+
"dev": true,
|
| 882 |
+
"license": "MIT",
|
| 883 |
+
"dependencies": {
|
| 884 |
+
"@rolldown/pluginutils": "1.0.0-rc.2"
|
| 885 |
+
},
|
| 886 |
+
"engines": {
|
| 887 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 888 |
+
},
|
| 889 |
+
"peerDependencies": {
|
| 890 |
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0",
|
| 891 |
+
"vue": "^3.2.25"
|
| 892 |
+
}
|
| 893 |
+
},
|
| 894 |
+
"node_modules/@vue/compiler-core": {
|
| 895 |
+
"version": "3.5.27",
|
| 896 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz",
|
| 897 |
+
"integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==",
|
| 898 |
+
"license": "MIT",
|
| 899 |
+
"dependencies": {
|
| 900 |
+
"@babel/parser": "^7.28.5",
|
| 901 |
+
"@vue/shared": "3.5.27",
|
| 902 |
+
"entities": "^7.0.0",
|
| 903 |
+
"estree-walker": "^2.0.2",
|
| 904 |
+
"source-map-js": "^1.2.1"
|
| 905 |
+
}
|
| 906 |
+
},
|
| 907 |
+
"node_modules/@vue/compiler-dom": {
|
| 908 |
+
"version": "3.5.27",
|
| 909 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz",
|
| 910 |
+
"integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==",
|
| 911 |
+
"license": "MIT",
|
| 912 |
+
"dependencies": {
|
| 913 |
+
"@vue/compiler-core": "3.5.27",
|
| 914 |
+
"@vue/shared": "3.5.27"
|
| 915 |
+
}
|
| 916 |
+
},
|
| 917 |
+
"node_modules/@vue/compiler-sfc": {
|
| 918 |
+
"version": "3.5.27",
|
| 919 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz",
|
| 920 |
+
"integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==",
|
| 921 |
+
"license": "MIT",
|
| 922 |
+
"dependencies": {
|
| 923 |
+
"@babel/parser": "^7.28.5",
|
| 924 |
+
"@vue/compiler-core": "3.5.27",
|
| 925 |
+
"@vue/compiler-dom": "3.5.27",
|
| 926 |
+
"@vue/compiler-ssr": "3.5.27",
|
| 927 |
+
"@vue/shared": "3.5.27",
|
| 928 |
+
"estree-walker": "^2.0.2",
|
| 929 |
+
"magic-string": "^0.30.21",
|
| 930 |
+
"postcss": "^8.5.6",
|
| 931 |
+
"source-map-js": "^1.2.1"
|
| 932 |
+
}
|
| 933 |
+
},
|
| 934 |
+
"node_modules/@vue/compiler-ssr": {
|
| 935 |
+
"version": "3.5.27",
|
| 936 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz",
|
| 937 |
+
"integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==",
|
| 938 |
+
"license": "MIT",
|
| 939 |
+
"dependencies": {
|
| 940 |
+
"@vue/compiler-dom": "3.5.27",
|
| 941 |
+
"@vue/shared": "3.5.27"
|
| 942 |
+
}
|
| 943 |
+
},
|
| 944 |
+
"node_modules/@vue/reactivity": {
|
| 945 |
+
"version": "3.5.27",
|
| 946 |
+
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz",
|
| 947 |
+
"integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==",
|
| 948 |
+
"license": "MIT",
|
| 949 |
+
"dependencies": {
|
| 950 |
+
"@vue/shared": "3.5.27"
|
| 951 |
+
}
|
| 952 |
+
},
|
| 953 |
+
"node_modules/@vue/runtime-core": {
|
| 954 |
+
"version": "3.5.27",
|
| 955 |
+
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz",
|
| 956 |
+
"integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==",
|
| 957 |
+
"license": "MIT",
|
| 958 |
+
"dependencies": {
|
| 959 |
+
"@vue/reactivity": "3.5.27",
|
| 960 |
+
"@vue/shared": "3.5.27"
|
| 961 |
+
}
|
| 962 |
+
},
|
| 963 |
+
"node_modules/@vue/runtime-dom": {
|
| 964 |
+
"version": "3.5.27",
|
| 965 |
+
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz",
|
| 966 |
+
"integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==",
|
| 967 |
+
"license": "MIT",
|
| 968 |
+
"dependencies": {
|
| 969 |
+
"@vue/reactivity": "3.5.27",
|
| 970 |
+
"@vue/runtime-core": "3.5.27",
|
| 971 |
+
"@vue/shared": "3.5.27",
|
| 972 |
+
"csstype": "^3.2.3"
|
| 973 |
+
}
|
| 974 |
+
},
|
| 975 |
+
"node_modules/@vue/server-renderer": {
|
| 976 |
+
"version": "3.5.27",
|
| 977 |
+
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz",
|
| 978 |
+
"integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==",
|
| 979 |
+
"license": "MIT",
|
| 980 |
+
"dependencies": {
|
| 981 |
+
"@vue/compiler-ssr": "3.5.27",
|
| 982 |
+
"@vue/shared": "3.5.27"
|
| 983 |
+
},
|
| 984 |
+
"peerDependencies": {
|
| 985 |
+
"vue": "3.5.27"
|
| 986 |
+
}
|
| 987 |
+
},
|
| 988 |
+
"node_modules/@vue/shared": {
|
| 989 |
+
"version": "3.5.27",
|
| 990 |
+
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz",
|
| 991 |
+
"integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==",
|
| 992 |
+
"license": "MIT"
|
| 993 |
+
},
|
| 994 |
+
"node_modules/csstype": {
|
| 995 |
+
"version": "3.2.3",
|
| 996 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 997 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 998 |
+
"license": "MIT"
|
| 999 |
+
},
|
| 1000 |
+
"node_modules/entities": {
|
| 1001 |
+
"version": "7.0.1",
|
| 1002 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
| 1003 |
+
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
| 1004 |
+
"license": "BSD-2-Clause",
|
| 1005 |
+
"engines": {
|
| 1006 |
+
"node": ">=0.12"
|
| 1007 |
+
},
|
| 1008 |
+
"funding": {
|
| 1009 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 1010 |
+
}
|
| 1011 |
+
},
|
| 1012 |
+
"node_modules/esbuild": {
|
| 1013 |
+
"version": "0.27.3",
|
| 1014 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
| 1015 |
+
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
| 1016 |
+
"dev": true,
|
| 1017 |
+
"hasInstallScript": true,
|
| 1018 |
+
"license": "MIT",
|
| 1019 |
+
"bin": {
|
| 1020 |
+
"esbuild": "bin/esbuild"
|
| 1021 |
+
},
|
| 1022 |
+
"engines": {
|
| 1023 |
+
"node": ">=18"
|
| 1024 |
+
},
|
| 1025 |
+
"optionalDependencies": {
|
| 1026 |
+
"@esbuild/aix-ppc64": "0.27.3",
|
| 1027 |
+
"@esbuild/android-arm": "0.27.3",
|
| 1028 |
+
"@esbuild/android-arm64": "0.27.3",
|
| 1029 |
+
"@esbuild/android-x64": "0.27.3",
|
| 1030 |
+
"@esbuild/darwin-arm64": "0.27.3",
|
| 1031 |
+
"@esbuild/darwin-x64": "0.27.3",
|
| 1032 |
+
"@esbuild/freebsd-arm64": "0.27.3",
|
| 1033 |
+
"@esbuild/freebsd-x64": "0.27.3",
|
| 1034 |
+
"@esbuild/linux-arm": "0.27.3",
|
| 1035 |
+
"@esbuild/linux-arm64": "0.27.3",
|
| 1036 |
+
"@esbuild/linux-ia32": "0.27.3",
|
| 1037 |
+
"@esbuild/linux-loong64": "0.27.3",
|
| 1038 |
+
"@esbuild/linux-mips64el": "0.27.3",
|
| 1039 |
+
"@esbuild/linux-ppc64": "0.27.3",
|
| 1040 |
+
"@esbuild/linux-riscv64": "0.27.3",
|
| 1041 |
+
"@esbuild/linux-s390x": "0.27.3",
|
| 1042 |
+
"@esbuild/linux-x64": "0.27.3",
|
| 1043 |
+
"@esbuild/netbsd-arm64": "0.27.3",
|
| 1044 |
+
"@esbuild/netbsd-x64": "0.27.3",
|
| 1045 |
+
"@esbuild/openbsd-arm64": "0.27.3",
|
| 1046 |
+
"@esbuild/openbsd-x64": "0.27.3",
|
| 1047 |
+
"@esbuild/openharmony-arm64": "0.27.3",
|
| 1048 |
+
"@esbuild/sunos-x64": "0.27.3",
|
| 1049 |
+
"@esbuild/win32-arm64": "0.27.3",
|
| 1050 |
+
"@esbuild/win32-ia32": "0.27.3",
|
| 1051 |
+
"@esbuild/win32-x64": "0.27.3"
|
| 1052 |
+
}
|
| 1053 |
+
},
|
| 1054 |
+
"node_modules/estree-walker": {
|
| 1055 |
+
"version": "2.0.2",
|
| 1056 |
+
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
| 1057 |
+
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
| 1058 |
+
"license": "MIT"
|
| 1059 |
+
},
|
| 1060 |
+
"node_modules/fdir": {
|
| 1061 |
+
"version": "6.5.0",
|
| 1062 |
+
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
| 1063 |
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
| 1064 |
+
"dev": true,
|
| 1065 |
+
"license": "MIT",
|
| 1066 |
+
"engines": {
|
| 1067 |
+
"node": ">=12.0.0"
|
| 1068 |
+
},
|
| 1069 |
+
"peerDependencies": {
|
| 1070 |
+
"picomatch": "^3 || ^4"
|
| 1071 |
+
},
|
| 1072 |
+
"peerDependenciesMeta": {
|
| 1073 |
+
"picomatch": {
|
| 1074 |
+
"optional": true
|
| 1075 |
+
}
|
| 1076 |
+
}
|
| 1077 |
+
},
|
| 1078 |
+
"node_modules/fsevents": {
|
| 1079 |
+
"version": "2.3.3",
|
| 1080 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1081 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1082 |
+
"dev": true,
|
| 1083 |
+
"hasInstallScript": true,
|
| 1084 |
+
"license": "MIT",
|
| 1085 |
+
"optional": true,
|
| 1086 |
+
"os": [
|
| 1087 |
+
"darwin"
|
| 1088 |
+
],
|
| 1089 |
+
"engines": {
|
| 1090 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1091 |
+
}
|
| 1092 |
+
},
|
| 1093 |
+
"node_modules/magic-string": {
|
| 1094 |
+
"version": "0.30.21",
|
| 1095 |
+
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
| 1096 |
+
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
| 1097 |
+
"license": "MIT",
|
| 1098 |
+
"dependencies": {
|
| 1099 |
+
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 1100 |
+
}
|
| 1101 |
+
},
|
| 1102 |
+
"node_modules/nanoid": {
|
| 1103 |
+
"version": "3.3.11",
|
| 1104 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 1105 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 1106 |
+
"funding": [
|
| 1107 |
+
{
|
| 1108 |
+
"type": "github",
|
| 1109 |
+
"url": "https://github.com/sponsors/ai"
|
| 1110 |
+
}
|
| 1111 |
+
],
|
| 1112 |
+
"license": "MIT",
|
| 1113 |
+
"bin": {
|
| 1114 |
+
"nanoid": "bin/nanoid.cjs"
|
| 1115 |
+
},
|
| 1116 |
+
"engines": {
|
| 1117 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 1118 |
+
}
|
| 1119 |
+
},
|
| 1120 |
+
"node_modules/picocolors": {
|
| 1121 |
+
"version": "1.1.1",
|
| 1122 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1123 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1124 |
+
"license": "ISC"
|
| 1125 |
+
},
|
| 1126 |
+
"node_modules/picomatch": {
|
| 1127 |
+
"version": "4.0.3",
|
| 1128 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
| 1129 |
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 1130 |
+
"dev": true,
|
| 1131 |
+
"license": "MIT",
|
| 1132 |
+
"engines": {
|
| 1133 |
+
"node": ">=12"
|
| 1134 |
+
},
|
| 1135 |
+
"funding": {
|
| 1136 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 1137 |
+
}
|
| 1138 |
+
},
|
| 1139 |
+
"node_modules/plotly.js-dist-min": {
|
| 1140 |
+
"version": "3.3.1",
|
| 1141 |
+
"resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-3.3.1.tgz",
|
| 1142 |
+
"integrity": "sha512-ZxKM9DlEoEF3wBzGRPGHt6gWTJrm5N81J9AgX9UBX/Qjc9L4lRxtPBPq+RmBJWoA71j1X5Z1ouuguLkdoo88tg==",
|
| 1143 |
+
"license": "MIT"
|
| 1144 |
+
},
|
| 1145 |
+
"node_modules/postcss": {
|
| 1146 |
+
"version": "8.5.6",
|
| 1147 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
| 1148 |
+
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
| 1149 |
+
"funding": [
|
| 1150 |
+
{
|
| 1151 |
+
"type": "opencollective",
|
| 1152 |
+
"url": "https://opencollective.com/postcss/"
|
| 1153 |
+
},
|
| 1154 |
+
{
|
| 1155 |
+
"type": "tidelift",
|
| 1156 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1157 |
+
},
|
| 1158 |
+
{
|
| 1159 |
+
"type": "github",
|
| 1160 |
+
"url": "https://github.com/sponsors/ai"
|
| 1161 |
+
}
|
| 1162 |
+
],
|
| 1163 |
+
"license": "MIT",
|
| 1164 |
+
"dependencies": {
|
| 1165 |
+
"nanoid": "^3.3.11",
|
| 1166 |
+
"picocolors": "^1.1.1",
|
| 1167 |
+
"source-map-js": "^1.2.1"
|
| 1168 |
+
},
|
| 1169 |
+
"engines": {
|
| 1170 |
+
"node": "^10 || ^12 || >=14"
|
| 1171 |
+
}
|
| 1172 |
+
},
|
| 1173 |
+
"node_modules/rollup": {
|
| 1174 |
+
"version": "4.57.1",
|
| 1175 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
|
| 1176 |
+
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
| 1177 |
+
"dev": true,
|
| 1178 |
+
"license": "MIT",
|
| 1179 |
+
"dependencies": {
|
| 1180 |
+
"@types/estree": "1.0.8"
|
| 1181 |
+
},
|
| 1182 |
+
"bin": {
|
| 1183 |
+
"rollup": "dist/bin/rollup"
|
| 1184 |
+
},
|
| 1185 |
+
"engines": {
|
| 1186 |
+
"node": ">=18.0.0",
|
| 1187 |
+
"npm": ">=8.0.0"
|
| 1188 |
+
},
|
| 1189 |
+
"optionalDependencies": {
|
| 1190 |
+
"@rollup/rollup-android-arm-eabi": "4.57.1",
|
| 1191 |
+
"@rollup/rollup-android-arm64": "4.57.1",
|
| 1192 |
+
"@rollup/rollup-darwin-arm64": "4.57.1",
|
| 1193 |
+
"@rollup/rollup-darwin-x64": "4.57.1",
|
| 1194 |
+
"@rollup/rollup-freebsd-arm64": "4.57.1",
|
| 1195 |
+
"@rollup/rollup-freebsd-x64": "4.57.1",
|
| 1196 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
|
| 1197 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
|
| 1198 |
+
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
|
| 1199 |
+
"@rollup/rollup-linux-arm64-musl": "4.57.1",
|
| 1200 |
+
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
|
| 1201 |
+
"@rollup/rollup-linux-loong64-musl": "4.57.1",
|
| 1202 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
|
| 1203 |
+
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
|
| 1204 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
|
| 1205 |
+
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
|
| 1206 |
+
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
|
| 1207 |
+
"@rollup/rollup-linux-x64-gnu": "4.57.1",
|
| 1208 |
+
"@rollup/rollup-linux-x64-musl": "4.57.1",
|
| 1209 |
+
"@rollup/rollup-openbsd-x64": "4.57.1",
|
| 1210 |
+
"@rollup/rollup-openharmony-arm64": "4.57.1",
|
| 1211 |
+
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
|
| 1212 |
+
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
|
| 1213 |
+
"@rollup/rollup-win32-x64-gnu": "4.57.1",
|
| 1214 |
+
"@rollup/rollup-win32-x64-msvc": "4.57.1",
|
| 1215 |
+
"fsevents": "~2.3.2"
|
| 1216 |
+
}
|
| 1217 |
+
},
|
| 1218 |
+
"node_modules/source-map-js": {
|
| 1219 |
+
"version": "1.2.1",
|
| 1220 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1221 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1222 |
+
"license": "BSD-3-Clause",
|
| 1223 |
+
"engines": {
|
| 1224 |
+
"node": ">=0.10.0"
|
| 1225 |
+
}
|
| 1226 |
+
},
|
| 1227 |
+
"node_modules/tinyglobby": {
|
| 1228 |
+
"version": "0.2.15",
|
| 1229 |
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
| 1230 |
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
| 1231 |
+
"dev": true,
|
| 1232 |
+
"license": "MIT",
|
| 1233 |
+
"dependencies": {
|
| 1234 |
+
"fdir": "^6.5.0",
|
| 1235 |
+
"picomatch": "^4.0.3"
|
| 1236 |
+
},
|
| 1237 |
+
"engines": {
|
| 1238 |
+
"node": ">=12.0.0"
|
| 1239 |
+
},
|
| 1240 |
+
"funding": {
|
| 1241 |
+
"url": "https://github.com/sponsors/SuperchupuDev"
|
| 1242 |
+
}
|
| 1243 |
+
},
|
| 1244 |
+
"node_modules/vite": {
|
| 1245 |
+
"version": "7.3.1",
|
| 1246 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
| 1247 |
+
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
| 1248 |
+
"dev": true,
|
| 1249 |
+
"license": "MIT",
|
| 1250 |
+
"dependencies": {
|
| 1251 |
+
"esbuild": "^0.27.0",
|
| 1252 |
+
"fdir": "^6.5.0",
|
| 1253 |
+
"picomatch": "^4.0.3",
|
| 1254 |
+
"postcss": "^8.5.6",
|
| 1255 |
+
"rollup": "^4.43.0",
|
| 1256 |
+
"tinyglobby": "^0.2.15"
|
| 1257 |
+
},
|
| 1258 |
+
"bin": {
|
| 1259 |
+
"vite": "bin/vite.js"
|
| 1260 |
+
},
|
| 1261 |
+
"engines": {
|
| 1262 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1263 |
+
},
|
| 1264 |
+
"funding": {
|
| 1265 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 1266 |
+
},
|
| 1267 |
+
"optionalDependencies": {
|
| 1268 |
+
"fsevents": "~2.3.3"
|
| 1269 |
+
},
|
| 1270 |
+
"peerDependencies": {
|
| 1271 |
+
"@types/node": "^20.19.0 || >=22.12.0",
|
| 1272 |
+
"jiti": ">=1.21.0",
|
| 1273 |
+
"less": "^4.0.0",
|
| 1274 |
+
"lightningcss": "^1.21.0",
|
| 1275 |
+
"sass": "^1.70.0",
|
| 1276 |
+
"sass-embedded": "^1.70.0",
|
| 1277 |
+
"stylus": ">=0.54.8",
|
| 1278 |
+
"sugarss": "^5.0.0",
|
| 1279 |
+
"terser": "^5.16.0",
|
| 1280 |
+
"tsx": "^4.8.1",
|
| 1281 |
+
"yaml": "^2.4.2"
|
| 1282 |
+
},
|
| 1283 |
+
"peerDependenciesMeta": {
|
| 1284 |
+
"@types/node": {
|
| 1285 |
+
"optional": true
|
| 1286 |
+
},
|
| 1287 |
+
"jiti": {
|
| 1288 |
+
"optional": true
|
| 1289 |
+
},
|
| 1290 |
+
"less": {
|
| 1291 |
+
"optional": true
|
| 1292 |
+
},
|
| 1293 |
+
"lightningcss": {
|
| 1294 |
+
"optional": true
|
| 1295 |
+
},
|
| 1296 |
+
"sass": {
|
| 1297 |
+
"optional": true
|
| 1298 |
+
},
|
| 1299 |
+
"sass-embedded": {
|
| 1300 |
+
"optional": true
|
| 1301 |
+
},
|
| 1302 |
+
"stylus": {
|
| 1303 |
+
"optional": true
|
| 1304 |
+
},
|
| 1305 |
+
"sugarss": {
|
| 1306 |
+
"optional": true
|
| 1307 |
+
},
|
| 1308 |
+
"terser": {
|
| 1309 |
+
"optional": true
|
| 1310 |
+
},
|
| 1311 |
+
"tsx": {
|
| 1312 |
+
"optional": true
|
| 1313 |
+
},
|
| 1314 |
+
"yaml": {
|
| 1315 |
+
"optional": true
|
| 1316 |
+
}
|
| 1317 |
+
}
|
| 1318 |
+
},
|
| 1319 |
+
"node_modules/vue": {
|
| 1320 |
+
"version": "3.5.27",
|
| 1321 |
+
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz",
|
| 1322 |
+
"integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==",
|
| 1323 |
+
"license": "MIT",
|
| 1324 |
+
"dependencies": {
|
| 1325 |
+
"@vue/compiler-dom": "3.5.27",
|
| 1326 |
+
"@vue/compiler-sfc": "3.5.27",
|
| 1327 |
+
"@vue/runtime-dom": "3.5.27",
|
| 1328 |
+
"@vue/server-renderer": "3.5.27",
|
| 1329 |
+
"@vue/shared": "3.5.27"
|
| 1330 |
+
},
|
| 1331 |
+
"peerDependencies": {
|
| 1332 |
+
"typescript": "*"
|
| 1333 |
+
},
|
| 1334 |
+
"peerDependenciesMeta": {
|
| 1335 |
+
"typescript": {
|
| 1336 |
+
"optional": true
|
| 1337 |
+
}
|
| 1338 |
+
}
|
| 1339 |
+
}
|
| 1340 |
+
}
|
| 1341 |
+
}
|
src/mcp-hub/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "mcp-hub",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"plotly.js-dist-min": "^3.3.1",
|
| 13 |
+
"vue": "^3.5.24"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"@vitejs/plugin-vue": "^6.0.1",
|
| 17 |
+
"vite": "^7.2.4"
|
| 18 |
+
}
|
| 19 |
+
}
|
src/mcp-hub/public/logo-icon.svg
ADDED
|
|
src/mcp-hub/public/logo.svg
ADDED
|
|
src/mcp-hub/public/vite.svg
ADDED
|
|
src/mcp-hub/src/App.vue
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<template>
|
| 3 |
+
<div id="mcp-hub">
|
| 4 |
+
<nav class="top-nav">
|
| 5 |
+
<div class="nav-brand">
|
| 6 |
+
<svg class="brand-logo" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 7 |
+
<g clip-path="url(#clip0_19_13)">
|
| 8 |
+
<path d="M18 84.8528L85.8822 16.9706C95.2548 7.59798 110.451 7.59798 119.823 16.9706V16.9706C129.196 26.3431 129.196 41.5391 119.823 50.9117L68.5581 102.177" stroke="currentColor" stroke-width="12" stroke-linecap="round"/>
|
| 9 |
+
<path d="M69.2652 101.47L119.823 50.9117C129.196 41.5391 144.392 41.5391 153.765 50.9117L154.118 51.2652C163.491 60.6378 163.491 75.8338 154.118 85.2063L92.7248 146.6C89.6006 149.724 89.6006 154.789 92.7248 157.913L105.331 170.52" stroke="currentColor" stroke-width="12" stroke-linecap="round"/>
|
| 10 |
+
<path d="M102.853 33.9411L52.6482 84.1457C43.2756 93.5183 43.2756 108.714 52.6482 118.087V118.087C62.0208 127.459 77.2167 127.459 86.5893 118.087L136.794 67.8822" stroke="currentColor" stroke-width="12" stroke-linecap="round"/>
|
| 11 |
+
</g>
|
| 12 |
+
<defs>
|
| 13 |
+
<clipPath id="clip0_19_13"><rect width="180" height="180" fill="white"/></clipPath>
|
| 14 |
+
</defs>
|
| 15 |
+
</svg>
|
| 16 |
+
MCP<span>HUB</span>
|
| 17 |
+
</div>
|
| 18 |
+
<div class="system-stats">
|
| 19 |
+
<span class="pulse-dot"></span>
|
| 20 |
+
{{ servers.length }} SERVERS ACTIVE
|
| 21 |
+
</div>
|
| 22 |
+
</nav>
|
| 23 |
+
|
| 24 |
+
<main class="dashboard-content">
|
| 25 |
+
<div v-if="!selectedServer">
|
| 26 |
+
<div class="summary-bar">
|
| 27 |
+
<div class="summary-item">
|
| 28 |
+
<span class="label">UPTIME</span>
|
| 29 |
+
<span class="value">{{ system.uptime }}</span>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="summary-item">
|
| 32 |
+
<span class="label">THROUGHPUT</span>
|
| 33 |
+
<span class="value">{{ system.throughput }}</span>
|
| 34 |
+
</div>
|
| 35 |
+
<div class="summary-item">
|
| 36 |
+
<span class="label">LATENCY</span>
|
| 37 |
+
<span class="value">{{ system.latency }}</span>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<section class="trend-section">
|
| 42 |
+
<div class="trend-header">
|
| 43 |
+
<div class="v-header">USAGE TRENDS</div>
|
| 44 |
+
<div class="range-selector">
|
| 45 |
+
<button v-for="r in ranges" :key="r" :class="{ active: selectedRange === r }" @click="setRange(r)">{{ r.toUpperCase() }}</button>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
<!-- ... (SVG remains same) ... -->
|
| 49 |
+
<div class="trend-container">
|
| 50 |
+
<div class="y-axis">
|
| 51 |
+
<span v-for="tick in yTicks" :key="tick">{{ tick }}</span>
|
| 52 |
+
</div>
|
| 53 |
+
<div class="trend-chart">
|
| 54 |
+
<svg viewBox="0 0 1000 120" class="sparkline" @mousemove="handleHover" @mouseleave="hoverInfo = null">
|
| 55 |
+
<line v-for="tick in [0, 33, 66, 100]" :key="tick" x1="0" :y1="110 - tick" x2="1000" :y2="110 - tick" stroke="var(--border)" stroke-width="1" stroke-dasharray="4,4" />
|
| 56 |
+
<path v-for="chart in getCharts" :key="chart.name" :d="chart.path" fill="none" :stroke="chart.color" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="trend-path" />
|
| 57 |
+
<line v-if="hoverInfo" :x1="hoverInfo.x" y1="0" :x2="hoverInfo.x" y2="120" stroke="var(--accent)" stroke-width="1" stroke-dasharray="2,2" />
|
| 58 |
+
</svg>
|
| 59 |
+
|
| 60 |
+
<div v-if="hoverInfo" class="chart-tooltip" :style="{ left: (hoverInfo.x / 10) + '%' }">
|
| 61 |
+
<div class="tooltip-header">{{ hoverInfo.label }}</div>
|
| 62 |
+
<div class="tooltip-body">
|
| 63 |
+
<div v-for="entry in hoverInfo.entries" :key="entry.name" class="tooltip-row" v-show="entry.val > 0">
|
| 64 |
+
<span class="dot" :style="{ background: entry.color }"></span>
|
| 65 |
+
<span class="name">{{ entry.name }}</span>
|
| 66 |
+
<span class="val">{{ entry.val }}</span>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<div class="chart-labels">
|
| 72 |
+
<span v-for="(l, i) in visibleXLabels" :key="i">{{ l }}</span>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</section>
|
| 77 |
+
|
| 78 |
+
<div class="server-list">
|
| 79 |
+
<div v-for="server in servers" :key="server.id" class="server-row" @click="viewServer(server.id)">
|
| 80 |
+
<div class="row-status">
|
| 81 |
+
<span :class="['status-indicator', getStatusStatus(server.status)]"></span>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="row-info">
|
| 84 |
+
<div class="row-header">
|
| 85 |
+
<span class="server-id">{{ server.id.toUpperCase() }}</span>
|
| 86 |
+
<span class="server-name">{{ server.name }}</span>
|
| 87 |
+
</div>
|
| 88 |
+
<div class="server-desc">{{ server.description }}</div>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="row-metrics">
|
| 91 |
+
<div class="metric">
|
| 92 |
+
<span class="m-val">{{ server.metrics.hourly }}</span>
|
| 93 |
+
<span class="m-lab">1H</span>
|
| 94 |
+
</div>
|
| 95 |
+
<div class="metric">
|
| 96 |
+
<span class="m-val">{{ server.metrics.weekly }}</span>
|
| 97 |
+
<span class="m-lab">7D</span>
|
| 98 |
+
</div>
|
| 99 |
+
<div class="metric">
|
| 100 |
+
<span class="m-val">{{ server.metrics.monthly }}</span>
|
| 101 |
+
<span class="m-lab">30D</span>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
<div class="row-stage">
|
| 105 |
+
<span :class="['stage-badge', getStatusClass(server.status)]">{{ server.status }}</span>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<!-- Detail Deep Dive -->
|
| 112 |
+
<div v-else class="detail-view">
|
| 113 |
+
<header class="detail-header">
|
| 114 |
+
<button @click="closeServer" class="back-btn">← BACK TO OVERVIEW</button>
|
| 115 |
+
<div class="detail-title">
|
| 116 |
+
<span class="id-tag">{{ selectedServer.id.toUpperCase() }}</span>
|
| 117 |
+
<h1>{{ selectedServer.name }}</h1>
|
| 118 |
+
</div>
|
| 119 |
+
<div class="detail-actions">
|
| 120 |
+
<div class="status-indicator-pill">
|
| 121 |
+
<span class="pulse-dot"></span> INTEGRATED LOG STREAM
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
</header>
|
| 125 |
+
|
| 126 |
+
<div class="detail-grid">
|
| 127 |
+
<section class="doc-section">
|
| 128 |
+
<div class="v-header">DETAILS</div>
|
| 129 |
+
<p class="markdown-text">{{ selectedServer.description }}</p>
|
| 130 |
+
|
| 131 |
+
<div class="v-header" style="margin-top: 2rem;">TOOLS</div>
|
| 132 |
+
<ul class="tool-list">
|
| 133 |
+
<li v-for="tool in selectedServer.tools" :key="tool">
|
| 134 |
+
<code>{{ tool }}</code>
|
| 135 |
+
</li>
|
| 136 |
+
</ul>
|
| 137 |
+
</section>
|
| 138 |
+
|
| 139 |
+
<section class="code-section">
|
| 140 |
+
<div class="v-header">USAGE EXAMPLE (PYTHON)</div>
|
| 141 |
+
<div class="code-container">
|
| 142 |
+
<pre><code>{{ selectedServer.sample_code }}</code></pre>
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div class="v-header" style="margin-top: 2rem;">LIVE SYSTEM LOGS</div>
|
| 146 |
+
<div class="log-terminal">
|
| 147 |
+
<pre><code>{{ currentLogs }}</code></pre>
|
| 148 |
+
</div>
|
| 149 |
+
</section>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
</main>
|
| 153 |
+
</div>
|
| 154 |
+
</template>
|
| 155 |
+
|
| 156 |
+
<script setup>
|
| 157 |
+
import { onMounted, ref, computed } from 'vue'
|
| 158 |
+
|
| 159 |
+
const servers = ref([])
|
| 160 |
+
const system = ref({ uptime: '99.9%', throughput: '0/hr', latency: '0ms' })
|
| 161 |
+
const usageData = ref({ labels: [], datasets: [] })
|
| 162 |
+
const selectedServer = ref(null)
|
| 163 |
+
const currentLogs = ref('Initializing terminal...')
|
| 164 |
+
const selectedRange = ref('24h')
|
| 165 |
+
const ranges = ['1h', '24h', '7d', '30d']
|
| 166 |
+
const hoverInfo = ref(null)
|
| 167 |
+
let logTimer = null
|
| 168 |
+
|
| 169 |
+
const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#4ade80']
|
| 170 |
+
|
| 171 |
+
const viewServer = async (id) => {
|
| 172 |
+
try {
|
| 173 |
+
const res = await fetch(`/api/servers/${id}`)
|
| 174 |
+
selectedServer.value = await res.json()
|
| 175 |
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
| 176 |
+
fetchLogs(id)
|
| 177 |
+
logTimer = setInterval(() => fetchLogs(id), 5000)
|
| 178 |
+
} catch (e) {
|
| 179 |
+
console.error("Link Desync", e)
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
const closeServer = () => {
|
| 184 |
+
selectedServer.value = null
|
| 185 |
+
if (logTimer) clearInterval(logTimer)
|
| 186 |
+
currentLogs.value = 'Initializing terminal...'
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
const fetchLogs = async (id) => {
|
| 190 |
+
try {
|
| 191 |
+
const res = await fetch(`/api/servers/${id}/logs`)
|
| 192 |
+
const data = await res.json()
|
| 193 |
+
currentLogs.value = data.logs || 'No active log feed.'
|
| 194 |
+
} catch (e) {
|
| 195 |
+
currentLogs.value = 'Neural link disrupted. Retrying...'
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
const setRange = (r) => {
|
| 200 |
+
selectedRange.value = r
|
| 201 |
+
fetchUsage()
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
const fetchUsage = async () => {
|
| 205 |
+
try {
|
| 206 |
+
const res = await fetch(`/api/usage?range=${selectedRange.value}`)
|
| 207 |
+
usageData.value = await res.json()
|
| 208 |
+
} catch (e) {
|
| 209 |
+
console.error("Usage Desync", e)
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
const maxUsage = computed(() => {
|
| 214 |
+
if (!usageData.value.datasets.length) return 1
|
| 215 |
+
return Math.max(...usageData.value.datasets.flatMap(ds => ds.data), 1)
|
| 216 |
+
})
|
| 217 |
+
|
| 218 |
+
const yTicks = computed(() => {
|
| 219 |
+
const max = maxUsage.value
|
| 220 |
+
return [max, Math.floor(max * 0.66), Math.floor(max * 0.33), 0]
|
| 221 |
+
})
|
| 222 |
+
|
| 223 |
+
const visibleXLabels = computed(() => {
|
| 224 |
+
const labels = usageData.value.labels
|
| 225 |
+
if (!labels.length) return []
|
| 226 |
+
|
| 227 |
+
// Target max 6 labels to prevent overcrowding
|
| 228 |
+
const len = labels.length
|
| 229 |
+
if (len <= 6) return labels
|
| 230 |
+
|
| 231 |
+
// Calculate a step that gives us roughly 6 ticks
|
| 232 |
+
// e.g. 24 items -> step 4 -> 6 items
|
| 233 |
+
const step = Math.ceil((len - 1) / 5)
|
| 234 |
+
|
| 235 |
+
return labels.map((l, i) => {
|
| 236 |
+
// Audit: Always show first and last. Show others if they match step.
|
| 237 |
+
if (i === 0 || i === len - 1 || i % step === 0) return l
|
| 238 |
+
return '' // Empty filtered label preserves flex spacing
|
| 239 |
+
})
|
| 240 |
+
})
|
| 241 |
+
|
| 242 |
+
const getCharts = computed(() => {
|
| 243 |
+
const labels = usageData.value.labels
|
| 244 |
+
const datasets = usageData.value.datasets
|
| 245 |
+
if (!labels.length || !datasets.length) return []
|
| 246 |
+
|
| 247 |
+
const step = 1000 / (labels.length - 1)
|
| 248 |
+
const max = maxUsage.value
|
| 249 |
+
|
| 250 |
+
return datasets.map((ds, idx) => {
|
| 251 |
+
const points = ds.data.map((v, i) => ({
|
| 252 |
+
x: i * step,
|
| 253 |
+
y: 110 - (v / max) * 100,
|
| 254 |
+
val: v,
|
| 255 |
+
label: labels[i]
|
| 256 |
+
}))
|
| 257 |
+
|
| 258 |
+
const d = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ')
|
| 259 |
+
return {
|
| 260 |
+
name: ds.name,
|
| 261 |
+
color: colors[idx % colors.length],
|
| 262 |
+
path: d,
|
| 263 |
+
points
|
| 264 |
+
}
|
| 265 |
+
})
|
| 266 |
+
})
|
| 267 |
+
|
| 268 |
+
const handleHover = (event) => {
|
| 269 |
+
const svg = event.currentTarget
|
| 270 |
+
const rect = svg.getBoundingClientRect()
|
| 271 |
+
const x = ((event.clientX - rect.left) / rect.width) * 1000
|
| 272 |
+
|
| 273 |
+
const labels = usageData.value.labels
|
| 274 |
+
if (!labels.length) return
|
| 275 |
+
const step = 1000 / (labels.length - 1)
|
| 276 |
+
const idx = Math.round(x / step)
|
| 277 |
+
|
| 278 |
+
if (idx >= 0 && idx < labels.length) {
|
| 279 |
+
const entries = usageData.value.datasets.map((ds, i) => ({
|
| 280 |
+
name: ds.name,
|
| 281 |
+
val: ds.data[idx],
|
| 282 |
+
color: colors[i % colors.length]
|
| 283 |
+
})).sort((a,b) => b.val - a.val)
|
| 284 |
+
|
| 285 |
+
hoverInfo.value = {
|
| 286 |
+
x: idx * step,
|
| 287 |
+
label: labels[idx],
|
| 288 |
+
entries
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
const sortedByUsage = computed(() => {
|
| 294 |
+
return [...servers.value].sort((a, b) => (b.metrics.raw_monthly || 0) - (a.metrics.raw_monthly || 0)).slice(0, 5)
|
| 295 |
+
})
|
| 296 |
+
|
| 297 |
+
const getStatusClass = (status) => {
|
| 298 |
+
if (!status) return 'stage-offline'
|
| 299 |
+
const s = status.toLowerCase()
|
| 300 |
+
if (s.includes('running')) return 'stage-online'
|
| 301 |
+
if (s.includes('sleeping') || s.includes('building')) return 'stage-warning'
|
| 302 |
+
return 'stage-offline'
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
const getStatusStatus = (status) => {
|
| 306 |
+
if (!status) return 's-offline'
|
| 307 |
+
const s = status.toLowerCase()
|
| 308 |
+
if (s.includes('running')) return 's-online'
|
| 309 |
+
if (s.includes('sleeping') || s.includes('building')) return 's-warning'
|
| 310 |
+
return 's-offline'
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
const fetchData = async () => {
|
| 314 |
+
try {
|
| 315 |
+
const res = await fetch('/api/servers')
|
| 316 |
+
const data = await res.json()
|
| 317 |
+
servers.value = data.servers
|
| 318 |
+
system.value = data.system
|
| 319 |
+
} catch (e) {
|
| 320 |
+
console.error("Link Desync", e)
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
onMounted(() => {
|
| 325 |
+
fetchData()
|
| 326 |
+
fetchUsage()
|
| 327 |
+
setInterval(() => {
|
| 328 |
+
fetchData()
|
| 329 |
+
fetchUsage()
|
| 330 |
+
}, 15000)
|
| 331 |
+
})
|
| 332 |
+
</script>
|
| 333 |
+
|
| 334 |
+
<style>
|
| 335 |
+
/* App specific overrides */
|
| 336 |
+
#mcp-hub {
|
| 337 |
+
width: 100%;
|
| 338 |
+
max-width: 1000px;
|
| 339 |
+
margin: 0 auto;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.brand-logo {
|
| 343 |
+
height: 28px;
|
| 344 |
+
width: 28px;
|
| 345 |
+
margin-right: 12px;
|
| 346 |
+
color: var(--accent);
|
| 347 |
+
}
|
| 348 |
+
</style>
|
src/mcp-hub/src/assets/vue.svg
ADDED
|
|
src/mcp-hub/src/components/HelloWorld.vue
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup>
|
| 2 |
+
import { ref } from 'vue'
|
| 3 |
+
|
| 4 |
+
defineProps({
|
| 5 |
+
msg: String,
|
| 6 |
+
})
|
| 7 |
+
|
| 8 |
+
const count = ref(0)
|
| 9 |
+
</script>
|
| 10 |
+
|
| 11 |
+
<template>
|
| 12 |
+
<h1>{{ msg }}</h1>
|
| 13 |
+
|
| 14 |
+
<div class="card">
|
| 15 |
+
<button type="button" @click="count++">count is {{ count }}</button>
|
| 16 |
+
<p>
|
| 17 |
+
Edit
|
| 18 |
+
<code>components/HelloWorld.vue</code> to test HMR
|
| 19 |
+
</p>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<p>
|
| 23 |
+
Check out
|
| 24 |
+
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
| 25 |
+
>create-vue</a
|
| 26 |
+
>, the official Vue + Vite starter
|
| 27 |
+
</p>
|
| 28 |
+
<p>
|
| 29 |
+
Learn more about IDE Support for Vue in the
|
| 30 |
+
<a
|
| 31 |
+
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
| 32 |
+
target="_blank"
|
| 33 |
+
>Vue Docs Scaling up Guide</a
|
| 34 |
+
>.
|
| 35 |
+
</p>
|
| 36 |
+
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
| 37 |
+
</template>
|
| 38 |
+
|
| 39 |
+
<style scoped>
|
| 40 |
+
.read-the-docs {
|
| 41 |
+
color: #888;
|
| 42 |
+
}
|
| 43 |
+
</style>
|
src/mcp-hub/src/main.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createApp } from 'vue'
|
| 2 |
+
import './style.css'
|
| 3 |
+
import App from './App.vue'
|
| 4 |
+
|
| 5 |
+
createApp(App).mount('#app')
|
src/mcp-hub/src/style.css
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&family=Inter:wght@400;500;700&display=swap');
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--bg-dark: #0a0908;
|
| 5 |
+
--bg-card: #1a1816;
|
| 6 |
+
--border: #2d2a26;
|
| 7 |
+
--accent: #cd7f32;
|
| 8 |
+
/* Copper */
|
| 9 |
+
--text-primary: #f5f2f0;
|
| 10 |
+
--text-dim: #a8a098;
|
| 11 |
+
--success: #10b981;
|
| 12 |
+
--warning: #f59e0b;
|
| 13 |
+
--error: #ef4444;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
* {
|
| 17 |
+
box-sizing: border-box;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
background-color: var(--bg-dark);
|
| 22 |
+
color: var(--text-primary);
|
| 23 |
+
font-family: 'Inter', sans-serif;
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 0;
|
| 26 |
+
-webkit-font-smoothing: antialiased;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/* Base Mobile Styles */
|
| 30 |
+
.top-nav {
|
| 31 |
+
display: flex;
|
| 32 |
+
align-items: center;
|
| 33 |
+
justify-content: space-between;
|
| 34 |
+
padding: 0.75rem 1.25rem;
|
| 35 |
+
background: var(--bg-card);
|
| 36 |
+
border-bottom: 1px solid var(--border);
|
| 37 |
+
position: sticky;
|
| 38 |
+
top: 0;
|
| 39 |
+
z-index: 1000;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.nav-brand {
|
| 43 |
+
font-family: 'Outfit', sans-serif;
|
| 44 |
+
font-weight: 800;
|
| 45 |
+
font-size: 1.1rem;
|
| 46 |
+
letter-spacing: -0.5px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.nav-brand span {
|
| 50 |
+
color: var(--accent);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.system-stats {
|
| 54 |
+
font-size: 0.65rem;
|
| 55 |
+
font-weight: 700;
|
| 56 |
+
color: var(--text-dim);
|
| 57 |
+
display: flex;
|
| 58 |
+
align-items: center;
|
| 59 |
+
gap: 0.4rem;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.pulse-dot {
|
| 63 |
+
width: 5px;
|
| 64 |
+
height: 5px;
|
| 65 |
+
background: var(--accent);
|
| 66 |
+
border-radius: 50%;
|
| 67 |
+
box-shadow: 0 0 6px var(--accent);
|
| 68 |
+
animation: pulse 2s infinite;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
@keyframes pulse {
|
| 72 |
+
0% {
|
| 73 |
+
opacity: 1;
|
| 74 |
+
transform: scale(1);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
50% {
|
| 78 |
+
opacity: 0.4;
|
| 79 |
+
transform: scale(1.2);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
100% {
|
| 83 |
+
opacity: 1;
|
| 84 |
+
transform: scale(1);
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.dashboard-content {
|
| 89 |
+
padding: 1rem;
|
| 90 |
+
max-width: 1200px;
|
| 91 |
+
margin: 0 auto;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.summary-bar {
|
| 95 |
+
display: grid;
|
| 96 |
+
grid-template-columns: repeat(2, 1fr);
|
| 97 |
+
gap: 1rem;
|
| 98 |
+
margin-bottom: 1.5rem;
|
| 99 |
+
background: var(--bg-card);
|
| 100 |
+
padding: 1rem;
|
| 101 |
+
border-radius: 12px;
|
| 102 |
+
border: 1px solid var(--border);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.summary-item:last-child {
|
| 106 |
+
grid-column: span 2;
|
| 107 |
+
border-top: 1px solid var(--border);
|
| 108 |
+
padding-top: 0.75rem;
|
| 109 |
+
text-align: center;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.summary-item .label {
|
| 113 |
+
font-size: 0.6rem;
|
| 114 |
+
font-weight: 700;
|
| 115 |
+
color: var(--text-dim);
|
| 116 |
+
letter-spacing: 0.5px;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.summary-item .value {
|
| 120 |
+
font-size: 1rem;
|
| 121 |
+
font-weight: 700;
|
| 122 |
+
color: #fff;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* Trend Visualizer - Mobile First */
|
| 126 |
+
.trend-section {
|
| 127 |
+
background: var(--bg-card);
|
| 128 |
+
border: 1px solid var(--border);
|
| 129 |
+
border-radius: 12px;
|
| 130 |
+
padding: 1rem;
|
| 131 |
+
margin-bottom: 1.5rem;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.trend-header {
|
| 135 |
+
display: flex;
|
| 136 |
+
flex-direction: column;
|
| 137 |
+
gap: 0.75rem;
|
| 138 |
+
margin-bottom: 1rem;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.v-header {
|
| 142 |
+
font-family: 'Outfit', sans-serif;
|
| 143 |
+
font-size: 0.65rem;
|
| 144 |
+
font-weight: 800;
|
| 145 |
+
color: var(--text-dim);
|
| 146 |
+
letter-spacing: 1px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.range-selector {
|
| 150 |
+
display: flex;
|
| 151 |
+
background: var(--bg-dark);
|
| 152 |
+
padding: 2px;
|
| 153 |
+
border-radius: 6px;
|
| 154 |
+
overflow-x: auto;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.range-selector button {
|
| 158 |
+
flex: 1;
|
| 159 |
+
background: transparent;
|
| 160 |
+
border: none;
|
| 161 |
+
color: var(--text-dim);
|
| 162 |
+
font-size: 0.6rem;
|
| 163 |
+
font-weight: 700;
|
| 164 |
+
padding: 6px 4px;
|
| 165 |
+
border-radius: 4px;
|
| 166 |
+
cursor: pointer;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.range-selector button.active {
|
| 170 |
+
background: var(--accent);
|
| 171 |
+
color: #fff;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.trend-container {
|
| 175 |
+
display: flex;
|
| 176 |
+
flex-direction: column;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.y-axis {
|
| 180 |
+
display: none;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
/* Hide Y-axis on narrow screens to save space */
|
| 184 |
+
|
| 185 |
+
.trend-chart {
|
| 186 |
+
position: relative;
|
| 187 |
+
height: 120px;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.sparkline {
|
| 191 |
+
width: 100%;
|
| 192 |
+
height: 100px;
|
| 193 |
+
cursor: crosshair;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.chart-labels {
|
| 197 |
+
display: flex;
|
| 198 |
+
justify-content: space-between;
|
| 199 |
+
margin-top: 0.25rem;
|
| 200 |
+
font-size: 0.5rem;
|
| 201 |
+
font-weight: 600;
|
| 202 |
+
color: var(--text-dim);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.chart-labels span:nth-child(even) {
|
| 206 |
+
display: none;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/* Show fewer labels on mobile */
|
| 210 |
+
|
| 211 |
+
.chart-tooltip {
|
| 212 |
+
position: absolute;
|
| 213 |
+
top: -10px;
|
| 214 |
+
transform: translateX(-50%);
|
| 215 |
+
background: rgba(9, 9, 11, 0.98);
|
| 216 |
+
border: 1px solid var(--accent);
|
| 217 |
+
border-radius: 6px;
|
| 218 |
+
padding: 0.5rem;
|
| 219 |
+
z-index: 100;
|
| 220 |
+
pointer-events: none;
|
| 221 |
+
min-width: 120px;
|
| 222 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.tooltip-header {
|
| 226 |
+
font-size: 0.55rem;
|
| 227 |
+
color: var(--text-dim);
|
| 228 |
+
margin-bottom: 0.25rem;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.tooltip-row {
|
| 232 |
+
display: flex;
|
| 233 |
+
align-items: center;
|
| 234 |
+
gap: 0.4rem;
|
| 235 |
+
margin-bottom: 0.15rem;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.tooltip-row .name {
|
| 239 |
+
font-size: 0.6rem;
|
| 240 |
+
flex: 1;
|
| 241 |
+
color: var(--text-primary);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.tooltip-row .val {
|
| 245 |
+
font-size: 0.65rem;
|
| 246 |
+
font-weight: 700;
|
| 247 |
+
color: #fff;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/* Server List - Mobile First Cards */
|
| 251 |
+
.server-list {
|
| 252 |
+
display: flex;
|
| 253 |
+
flex-direction: column;
|
| 254 |
+
gap: 1rem;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.server-row {
|
| 258 |
+
display: flex;
|
| 259 |
+
flex-direction: column;
|
| 260 |
+
background: var(--bg-card);
|
| 261 |
+
border: 1px solid var(--border);
|
| 262 |
+
border-radius: 12px;
|
| 263 |
+
padding: 1.25rem;
|
| 264 |
+
gap: 1rem;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.row-status {
|
| 268 |
+
display: none;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
/* Status indicator logic handled by badge */
|
| 272 |
+
|
| 273 |
+
.row-info {
|
| 274 |
+
border-bottom: 1px solid var(--border);
|
| 275 |
+
padding-bottom: 0.75rem;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.row-header {
|
| 279 |
+
display: flex;
|
| 280 |
+
flex-direction: column;
|
| 281 |
+
gap: 0.25rem;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.server-id {
|
| 285 |
+
font-family: 'Outfit', sans-serif;
|
| 286 |
+
font-size: 0.65rem;
|
| 287 |
+
color: var(--accent);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.server-name {
|
| 291 |
+
font-weight: 700;
|
| 292 |
+
font-size: 1rem;
|
| 293 |
+
color: #fff;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.server-desc {
|
| 297 |
+
font-size: 0.75rem;
|
| 298 |
+
color: var(--text-dim);
|
| 299 |
+
margin-top: 0.4rem;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.row-metrics {
|
| 303 |
+
display: flex;
|
| 304 |
+
justify-content: space-between;
|
| 305 |
+
background: var(--bg-dark);
|
| 306 |
+
padding: 0.75rem;
|
| 307 |
+
border-radius: 8px;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.metric {
|
| 311 |
+
flex: 1;
|
| 312 |
+
text-align: center;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.m-val {
|
| 316 |
+
display: block;
|
| 317 |
+
font-size: 0.85rem;
|
| 318 |
+
font-weight: 700;
|
| 319 |
+
color: #fff;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.m-lab {
|
| 323 |
+
display: block;
|
| 324 |
+
font-size: 0.55rem;
|
| 325 |
+
font-weight: 600;
|
| 326 |
+
color: var(--text-dim);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.row-stage {
|
| 330 |
+
text-align: right;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.stage-badge {
|
| 334 |
+
display: inline-block;
|
| 335 |
+
font-size: 0.6rem;
|
| 336 |
+
font-weight: 800;
|
| 337 |
+
padding: 4px 10px;
|
| 338 |
+
border-radius: 4px;
|
| 339 |
+
text-transform: uppercase;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.stage-online {
|
| 343 |
+
background: rgba(16, 185, 129, 0.1);
|
| 344 |
+
color: var(--success);
|
| 345 |
+
border: 1px solid var(--success);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.stage-warning {
|
| 349 |
+
background: rgba(245, 158, 11, 0.1);
|
| 350 |
+
color: var(--warning);
|
| 351 |
+
border: 1px solid var(--warning);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.stage-offline {
|
| 355 |
+
background: rgba(239, 68, 68, 0.1);
|
| 356 |
+
color: var(--error);
|
| 357 |
+
border: 1px solid var(--error);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/* Desktop Overrides */
|
| 361 |
+
@media (min-width: 768px) {
|
| 362 |
+
.dashboard-content {
|
| 363 |
+
padding: 2rem;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.summary-bar {
|
| 367 |
+
grid-template-columns: repeat(3, 1fr);
|
| 368 |
+
padding: 1.25rem 2rem;
|
| 369 |
+
gap: 2rem;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.summary-item:last-child {
|
| 373 |
+
grid-column: auto;
|
| 374 |
+
border-top: none;
|
| 375 |
+
padding-top: 0;
|
| 376 |
+
text-align: left;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.summary-item .value {
|
| 380 |
+
font-size: 1.25rem;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.trend-section {
|
| 384 |
+
padding: 1.5rem 2rem;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.trend-header {
|
| 388 |
+
flex-direction: row;
|
| 389 |
+
justify-content: space-between;
|
| 390 |
+
align-items: center;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.range-selector {
|
| 394 |
+
padding: 2px;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.range-selector button {
|
| 398 |
+
font-size: 0.65rem;
|
| 399 |
+
padding: 4px 12px;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.trend-container {
|
| 403 |
+
flex-direction: row;
|
| 404 |
+
gap: 1rem;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.y-axis {
|
| 408 |
+
display: flex;
|
| 409 |
+
flex-direction: column;
|
| 410 |
+
justify-content: space-between;
|
| 411 |
+
padding-bottom: 20px;
|
| 412 |
+
font-size: 0.6rem;
|
| 413 |
+
font-weight: 700;
|
| 414 |
+
color: var(--text-dim);
|
| 415 |
+
min-width: 30px;
|
| 416 |
+
text-align: right;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.trend-chart {
|
| 420 |
+
height: 160px;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.sparkline {
|
| 424 |
+
height: 140px;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.chart-labels span:nth-child(even) {
|
| 428 |
+
display: inline;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.chart-labels {
|
| 432 |
+
font-size: 0.6rem;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.server-row {
|
| 436 |
+
flex-direction: row;
|
| 437 |
+
align-items: center;
|
| 438 |
+
padding: 1rem 1.5rem;
|
| 439 |
+
display: grid;
|
| 440 |
+
grid-template-columns: 1fr 200px 140px;
|
| 441 |
+
gap: 2rem;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.row-info {
|
| 445 |
+
border-bottom: none;
|
| 446 |
+
padding-bottom: 0;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.server-desc {
|
| 450 |
+
max-width: 400px;
|
| 451 |
+
white-space: nowrap;
|
| 452 |
+
overflow: hidden;
|
| 453 |
+
text-overflow: ellipsis;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.row-metrics {
|
| 457 |
+
background: transparent;
|
| 458 |
+
padding: 0;
|
| 459 |
+
gap: 1.5rem;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
.m-val {
|
| 463 |
+
font-size: 0.9rem;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.server-row:hover {
|
| 467 |
+
border-color: var(--accent);
|
| 468 |
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
| 469 |
+
transform: translateY(-2px);
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
.trend-path {
|
| 473 |
+
stroke-width: 2px;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
.sparkline:hover .trend-path {
|
| 477 |
+
opacity: 0.2;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
.sparkline:hover .trend-path:hover {
|
| 481 |
+
opacity: 1;
|
| 482 |
+
stroke-width: 3px;
|
| 483 |
+
}
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
/* Detail Deep Dive */
|
| 487 |
+
.detail-view {
|
| 488 |
+
animation: fadeIn 0.4s ease;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
@keyframes fadeIn {
|
| 492 |
+
from {
|
| 493 |
+
opacity: 0;
|
| 494 |
+
transform: translateY(10px);
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
to {
|
| 498 |
+
opacity: 1;
|
| 499 |
+
transform: translateY(0);
|
| 500 |
+
}
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.detail-header {
|
| 504 |
+
display: flex;
|
| 505 |
+
flex-direction: column;
|
| 506 |
+
gap: 1.5rem;
|
| 507 |
+
margin-bottom: 2rem;
|
| 508 |
+
padding-bottom: 2rem;
|
| 509 |
+
border-bottom: 1px solid var(--border);
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.back-btn {
|
| 513 |
+
background: transparent;
|
| 514 |
+
border: 1px solid var(--border);
|
| 515 |
+
color: var(--text-dim);
|
| 516 |
+
font-size: 0.65rem;
|
| 517 |
+
font-weight: 800;
|
| 518 |
+
padding: 6px 12px;
|
| 519 |
+
border-radius: 4px;
|
| 520 |
+
cursor: pointer;
|
| 521 |
+
width: fit-content;
|
| 522 |
+
transition: all 0.2s;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.back-btn:hover {
|
| 526 |
+
border-color: var(--accent);
|
| 527 |
+
color: #fff;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.detail-title h1 {
|
| 531 |
+
font-family: 'Outfit', sans-serif;
|
| 532 |
+
font-size: 1.75rem;
|
| 533 |
+
margin: 0.5rem 0 0 0;
|
| 534 |
+
letter-spacing: -0.5px;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
.id-tag {
|
| 538 |
+
font-family: 'Outfit', sans-serif;
|
| 539 |
+
font-size: 0.7rem;
|
| 540 |
+
font-weight: 800;
|
| 541 |
+
color: var(--accent);
|
| 542 |
+
letter-spacing: 1px;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.logs-link {
|
| 546 |
+
display: inline-flex;
|
| 547 |
+
align-items: center;
|
| 548 |
+
gap: 0.75rem;
|
| 549 |
+
background: rgba(59, 130, 246, 0.1);
|
| 550 |
+
border: 1px solid var(--accent);
|
| 551 |
+
color: var(--accent);
|
| 552 |
+
padding: 0.75rem 1.25rem;
|
| 553 |
+
border-radius: 8px;
|
| 554 |
+
text-decoration: none;
|
| 555 |
+
font-size: 0.7rem;
|
| 556 |
+
font-weight: 800;
|
| 557 |
+
transition: all 0.2s;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.logs-link:hover {
|
| 561 |
+
background: var(--accent);
|
| 562 |
+
color: #fff;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.detail-grid {
|
| 566 |
+
display: grid;
|
| 567 |
+
grid-template-columns: 1fr;
|
| 568 |
+
gap: 2.5rem;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.markdown-text {
|
| 572 |
+
font-size: 0.95rem;
|
| 573 |
+
line-height: 1.6;
|
| 574 |
+
color: var(--text-dim);
|
| 575 |
+
margin-top: 1rem;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
.tool-list {
|
| 579 |
+
list-style: none;
|
| 580 |
+
padding: 0;
|
| 581 |
+
display: flex;
|
| 582 |
+
flex-wrap: wrap;
|
| 583 |
+
gap: 0.75rem;
|
| 584 |
+
margin-top: 1rem;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.tool-list code {
|
| 588 |
+
background: var(--bg-dark);
|
| 589 |
+
border: 1px solid var(--border);
|
| 590 |
+
color: var(--success);
|
| 591 |
+
padding: 4px 10px;
|
| 592 |
+
border-radius: 4px;
|
| 593 |
+
font-size: 0.75rem;
|
| 594 |
+
font-weight: 600;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.code-container {
|
| 598 |
+
background: #0d0d0f;
|
| 599 |
+
border: 1px solid var(--border);
|
| 600 |
+
border-radius: 12px;
|
| 601 |
+
padding: 1.25rem;
|
| 602 |
+
margin-top: 1rem;
|
| 603 |
+
overflow-x: auto;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.code-container code {
|
| 607 |
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
| 608 |
+
font-size: 0.8rem;
|
| 609 |
+
color: #9cdcfe;
|
| 610 |
+
line-height: 1.5;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
.endpoint-badge {
|
| 614 |
+
background: var(--bg-dark);
|
| 615 |
+
border: 1px solid var(--border);
|
| 616 |
+
color: var(--text-dim);
|
| 617 |
+
padding: 0.75rem;
|
| 618 |
+
border-radius: 8px;
|
| 619 |
+
font-family: monospace;
|
| 620 |
+
font-size: 0.75rem;
|
| 621 |
+
margin-top: 1rem;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
@media (min-width: 768px) {
|
| 625 |
+
.detail-header {
|
| 626 |
+
flex-direction: row;
|
| 627 |
+
align-items: flex-end;
|
| 628 |
+
justify-content: space-between;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.detail-grid {
|
| 632 |
+
grid-template-columns: 1fr 1fr;
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
.detail-title h1 {
|
| 636 |
+
font-size: 2.25rem;
|
| 637 |
+
}
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
.status-indicator-pill {
|
| 641 |
+
display: inline-flex;
|
| 642 |
+
align-items: center;
|
| 643 |
+
gap: 0.5rem;
|
| 644 |
+
background: rgba(205, 127, 50, 0.1);
|
| 645 |
+
border: 1px solid var(--accent);
|
| 646 |
+
color: var(--accent);
|
| 647 |
+
padding: 0.4rem 1rem;
|
| 648 |
+
border-radius: 64px;
|
| 649 |
+
font-size: 0.6rem;
|
| 650 |
+
font-weight: 800;
|
| 651 |
+
letter-spacing: 0.5px;
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
.log-terminal {
|
| 655 |
+
background: #050505;
|
| 656 |
+
border: 1px solid var(--border);
|
| 657 |
+
border-radius: 8px;
|
| 658 |
+
padding: 1rem;
|
| 659 |
+
margin-top: 1rem;
|
| 660 |
+
height: 200px;
|
| 661 |
+
overflow-y: auto;
|
| 662 |
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
| 663 |
+
font-size: 0.75rem;
|
| 664 |
+
color: #00ff41;
|
| 665 |
+
/* Classic Terminal Green */
|
| 666 |
+
line-height: 1.4;
|
| 667 |
+
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.8);
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
.log-terminal pre {
|
| 671 |
+
margin: 0;
|
| 672 |
+
white-space: pre-wrap;
|
| 673 |
+
word-break: break-all;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
@media (min-width: 768px) {
|
| 677 |
+
.log-terminal {
|
| 678 |
+
height: 300px;
|
| 679 |
+
}
|
| 680 |
+
}
|
src/mcp-hub/vite.config.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import vue from '@vitejs/plugin-vue'
|
| 3 |
+
|
| 4 |
+
// https://vite.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [vue()],
|
| 7 |
+
})
|
src/mcp-rag-secure/Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
git \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
COPY pyproject.toml .
|
| 11 |
+
RUN pip install --no-cache-dir .
|
| 12 |
+
|
| 13 |
+
COPY src/mcp-rag-secure ./src/mcp-rag-secure
|
| 14 |
+
COPY src/core ./src/core
|
| 15 |
+
|
| 16 |
+
ENV PYTHONPATH=/app/src
|
| 17 |
+
|
| 18 |
+
# Create directory for ChromaDB
|
| 19 |
+
RUN mkdir -p src/mcp-rag-secure/chroma_db && chmod 777 src/mcp-rag-secure/chroma_db
|
| 20 |
+
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
|
| 23 |
+
ENV MCP_TRANSPORT=sse
|
| 24 |
+
|
| 25 |
+
CMD ["python", "src/mcp-rag-secure/server.py"]
|
src/mcp-rag-secure/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
---
|
| 3 |
+
title: MCP Secure RAG
|
| 4 |
+
emoji: 🔒
|
| 5 |
+
colorFrom: pink
|
| 6 |
+
colorTo: red
|
| 7 |
+
sdk: docker
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# MCP Secure Multi-Tenant RAG Server
|
| 12 |
+
|
| 13 |
+
This is a Model Context Protocol (MCP) server for secure, tenant-isolated Retrieval-Augmented Generation.
|
| 14 |
+
|
| 15 |
+
## Tools
|
| 16 |
+
- `ingest_document`: Add documents with strict tenant ID metadata.
|
| 17 |
+
- `query_knowledge_base`: Query documents filtered by tenant ID.
|
| 18 |
+
- `delete_tenant_data`: Wipe data for a specific tenant.
|
| 19 |
+
|
| 20 |
+
## Security
|
| 21 |
+
- Uses ChromaDB for vector storage.
|
| 22 |
+
- All operations require a `tenant_id` to ensure data isolation.
|
| 23 |
+
|
| 24 |
+
## Running Locally
|
| 25 |
+
```bash
|
| 26 |
+
python src/mcp-rag-secure/server.py
|
| 27 |
+
```
|
src/mcp-rag-secure/server.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
Secure Multi-Tenant RAG MCP Server
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import uuid
|
| 8 |
+
import chromadb
|
| 9 |
+
from chromadb.config import Settings
|
| 10 |
+
from chromadb.utils import embedding_functions
|
| 11 |
+
from mcp.server.fastmcp import FastMCP
|
| 12 |
+
from typing import List, Dict, Any, Optional
|
| 13 |
+
from core.mcp_telemetry import log_usage
|
| 14 |
+
|
| 15 |
+
# Initialize FastMCP Server
|
| 16 |
+
mcp = FastMCP("Secure RAG", host="0.0.0.0")
|
| 17 |
+
|
| 18 |
+
# Initialize ChromaDB (Persistent)
|
| 19 |
+
# Store in src/mcp-rag-secure/chroma_db
|
| 20 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 21 |
+
persist_directory = os.path.join(current_dir, "chroma_db")
|
| 22 |
+
|
| 23 |
+
client = chromadb.PersistentClient(path=persist_directory)
|
| 24 |
+
|
| 25 |
+
# Use default embedding function (all-MiniLM-L6-v2 usually)
|
| 26 |
+
# Explicitly use SentenceTransformer if installed, else default
|
| 27 |
+
try:
|
| 28 |
+
from sentence_transformers import SentenceTransformer
|
| 29 |
+
# Custom embedding function wrapper
|
| 30 |
+
class SentenceTransformerEmbeddingFunction(embedding_functions.EmbeddingFunction):
|
| 31 |
+
def __init__(self, model_name="all-MiniLM-L6-v2"):
|
| 32 |
+
self.model = SentenceTransformer(model_name)
|
| 33 |
+
def __call__(self, input: List[str]) -> List[List[float]]:
|
| 34 |
+
return self.model.encode(input).tolist()
|
| 35 |
+
|
| 36 |
+
emb_fn = SentenceTransformerEmbeddingFunction()
|
| 37 |
+
except ImportError:
|
| 38 |
+
emb_fn = embedding_functions.DefaultEmbeddingFunction()
|
| 39 |
+
|
| 40 |
+
# Create collection
|
| 41 |
+
collection = client.get_or_create_collection(
|
| 42 |
+
name="secure_rag",
|
| 43 |
+
embedding_function=emb_fn
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
@mcp.tool()
|
| 47 |
+
def ingest_document(tenant_id: str, content: str, metadata: Dict[str, Any] = None) -> str:
|
| 48 |
+
"""
|
| 49 |
+
Ingest a document into the RAG system with strict tenant isolation.
|
| 50 |
+
"""
|
| 51 |
+
log_usage("mcp-rag-secure", "ingest_document")
|
| 52 |
+
if not metadata:
|
| 53 |
+
metadata = {}
|
| 54 |
+
|
| 55 |
+
# Enforce tenant_id in metadata
|
| 56 |
+
metadata["tenant_id"] = tenant_id
|
| 57 |
+
|
| 58 |
+
doc_id = str(uuid.uuid4())
|
| 59 |
+
|
| 60 |
+
collection.add(
|
| 61 |
+
documents=[content],
|
| 62 |
+
metadatas=[metadata],
|
| 63 |
+
ids=[doc_id]
|
| 64 |
+
)
|
| 65 |
+
return f"Document ingested with ID: {doc_id}"
|
| 66 |
+
|
| 67 |
+
@mcp.tool()
|
| 68 |
+
def query_knowledge_base(tenant_id: str, query: str, k: int = 3) -> List[Dict[str, Any]]:
|
| 69 |
+
"""
|
| 70 |
+
Query the knowledge base. Results are strictly filtered by tenant_id.
|
| 71 |
+
"""
|
| 72 |
+
log_usage("mcp-rag-secure", "query_knowledge_base")
|
| 73 |
+
results = collection.query(
|
| 74 |
+
query_texts=[query],
|
| 75 |
+
n_results=k,
|
| 76 |
+
where={"tenant_id": tenant_id} # Critical security filter
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
formatted_results = []
|
| 80 |
+
if results["documents"]:
|
| 81 |
+
for i, doc in enumerate(results["documents"][0]):
|
| 82 |
+
meta = results["metadatas"][0][i]
|
| 83 |
+
formatted_results.append({
|
| 84 |
+
"content": doc,
|
| 85 |
+
"metadata": meta,
|
| 86 |
+
"score": results["distances"][0][i] if results["distances"] else None
|
| 87 |
+
})
|
| 88 |
+
|
| 89 |
+
return formatted_results
|
| 90 |
+
|
| 91 |
+
@mcp.tool()
|
| 92 |
+
def delete_tenant_data(tenant_id: str) -> str:
|
| 93 |
+
"""
|
| 94 |
+
Delete all data associated with a specific tenant.
|
| 95 |
+
"""
|
| 96 |
+
collection.delete(
|
| 97 |
+
where={"tenant_id": tenant_id}
|
| 98 |
+
)
|
| 99 |
+
return f"All data for tenant {tenant_id} has been deleted."
|
| 100 |
+
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
import os
|
| 103 |
+
if os.environ.get("MCP_TRANSPORT") == "sse":
|
| 104 |
+
import uvicorn
|
| 105 |
+
port = int(os.environ.get("PORT", 7860))
|
| 106 |
+
uvicorn.run(mcp.sse_app(), host="0.0.0.0", port=port)
|
| 107 |
+
else:
|
| 108 |
+
mcp.run()
|
src/mcp-seo/Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
+
git \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
COPY pyproject.toml .
|
| 11 |
+
RUN pip install --no-cache-dir .
|
| 12 |
+
|
| 13 |
+
COPY src/mcp-seo ./src/mcp-seo
|
| 14 |
+
COPY src/core ./src/core
|
| 15 |
+
|
| 16 |
+
ENV PYTHONPATH=/app/src
|
| 17 |
+
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
ENV MCP_TRANSPORT=sse
|
| 21 |
+
|
| 22 |
+
CMD ["python", "src/mcp-seo/server.py"]
|
src/mcp-seo/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
---
|
| 3 |
+
title: MCP SEO & ADA
|
| 4 |
+
emoji: 🔍
|
| 5 |
+
colorFrom: purple
|
| 6 |
+
colorTo: pink
|
| 7 |
+
sdk: docker
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# MCP SEO & ADA Audit Server
|
| 12 |
+
|
| 13 |
+
This is a Model Context Protocol (MCP) server for website auditing, focusing on SEO and ADA/WCAG compliance.
|
| 14 |
+
|
| 15 |
+
## Tools
|
| 16 |
+
- `analyze_seo`: Basic SEO audit (Title, Meta, H1, Alt tags).
|
| 17 |
+
- `analyze_ada`: Accessibility compliance check (ARIA, lang, contrast proxies).
|
| 18 |
+
- `generate_sitemap`: Crawl and generate a list of internal links.
|
| 19 |
+
|
| 20 |
+
## Running Locally
|
| 21 |
+
```bash
|
| 22 |
+
python src/mcp-seo/server.py
|
| 23 |
+
```
|
src/mcp-seo/server.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
SEO & ADA Compliance MCP Server
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
import requests
|
| 8 |
+
from bs4 import BeautifulSoup
|
| 9 |
+
from urllib.parse import urljoin, urlparse
|
| 10 |
+
from mcp.server.fastmcp import FastMCP
|
| 11 |
+
from typing import List, Dict, Any, Set
|
| 12 |
+
from core.mcp_telemetry import log_usage
|
| 13 |
+
|
| 14 |
+
# Initialize FastMCP Server
|
| 15 |
+
mcp = FastMCP("SEO & ADA Audit", host="0.0.0.0")
|
| 16 |
+
|
| 17 |
+
@mcp.tool()
|
| 18 |
+
def analyze_seo(url: str) -> Dict[str, Any]:
|
| 19 |
+
"""
|
| 20 |
+
Perform a basic SEO audit of a webpage.
|
| 21 |
+
Checks title, meta description, H1 tags, image alt attributes, and internal/external links.
|
| 22 |
+
"""
|
| 23 |
+
log_usage("mcp-seo", "analyze_seo")
|
| 24 |
+
try:
|
| 25 |
+
response = requests.get(url, timeout=10)
|
| 26 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 27 |
+
|
| 28 |
+
result = {
|
| 29 |
+
"url": url,
|
| 30 |
+
"status_code": response.status_code,
|
| 31 |
+
"title": soup.title.string if soup.title else None,
|
| 32 |
+
"meta_description": None,
|
| 33 |
+
"h1_count": len(soup.find_all('h1')),
|
| 34 |
+
"images_missing_alt": 0,
|
| 35 |
+
"internal_links": 0,
|
| 36 |
+
"external_links": 0
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
# Meta Description
|
| 40 |
+
meta_desc = soup.find('meta', attrs={'name': 'description'})
|
| 41 |
+
if meta_desc:
|
| 42 |
+
result["meta_description"] = meta_desc.get('content')
|
| 43 |
+
|
| 44 |
+
# Images
|
| 45 |
+
imgs = soup.find_all('img')
|
| 46 |
+
for img in imgs:
|
| 47 |
+
if not img.get('alt'):
|
| 48 |
+
result["images_missing_alt"] += 1
|
| 49 |
+
|
| 50 |
+
# Links
|
| 51 |
+
links = soup.find_all('a', href=True)
|
| 52 |
+
domain = urlparse(url).netloc
|
| 53 |
+
for link in links:
|
| 54 |
+
href = link['href']
|
| 55 |
+
if href.startswith('/') or domain in href:
|
| 56 |
+
result["internal_links"] += 1
|
| 57 |
+
else:
|
| 58 |
+
result["external_links"] += 1
|
| 59 |
+
|
| 60 |
+
return result
|
| 61 |
+
except Exception as e:
|
| 62 |
+
return {"error": str(e)}
|
| 63 |
+
|
| 64 |
+
@mcp.tool()
|
| 65 |
+
def analyze_ada(url: str) -> Dict[str, Any]:
|
| 66 |
+
"""
|
| 67 |
+
Perform a basic ADA/WCAG accessibility check.
|
| 68 |
+
Checks for missing alt text, form labels, lang attribute, and ARIA usage.
|
| 69 |
+
"""
|
| 70 |
+
log_usage("mcp-seo", "analyze_ada")
|
| 71 |
+
try:
|
| 72 |
+
response = requests.get(url, timeout=10)
|
| 73 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 74 |
+
|
| 75 |
+
issues = []
|
| 76 |
+
|
| 77 |
+
# 1. Images missing alt
|
| 78 |
+
imgs = soup.find_all('img')
|
| 79 |
+
missing_alt = [img.get('src', 'unknown') for img in imgs if not img.get('alt')]
|
| 80 |
+
if missing_alt:
|
| 81 |
+
issues.append(f"Found {len(missing_alt)} images missing alt text.")
|
| 82 |
+
|
| 83 |
+
# 2. Html Lang attribute
|
| 84 |
+
html_tag = soup.find('html')
|
| 85 |
+
if not html_tag or not html_tag.get('lang'):
|
| 86 |
+
issues.append("Missing 'lang' attribute on <html> tag.")
|
| 87 |
+
|
| 88 |
+
# 3. Form input labels
|
| 89 |
+
inputs = soup.find_all('input')
|
| 90 |
+
for inp in inputs:
|
| 91 |
+
# Check if input has id and a corresponding label
|
| 92 |
+
inp_id = inp.get('id')
|
| 93 |
+
label = soup.find('label', attrs={'for': inp_id}) if inp_id else None
|
| 94 |
+
# Or parent is label
|
| 95 |
+
parent_label = inp.find_parent('label')
|
| 96 |
+
# Or aria-label
|
| 97 |
+
aria_label = inp.get('aria-label')
|
| 98 |
+
|
| 99 |
+
if not (label or parent_label or aria_label):
|
| 100 |
+
issues.append(f"Input field (type={inp.get('type')}) missing label.")
|
| 101 |
+
|
| 102 |
+
return {
|
| 103 |
+
"url": url,
|
| 104 |
+
"compliance_score": max(0, 100 - (len(issues) * 10)), # Rough score
|
| 105 |
+
"issues": issues
|
| 106 |
+
}
|
| 107 |
+
except Exception as e:
|
| 108 |
+
return {"error": str(e)}
|
| 109 |
+
|
| 110 |
+
@mcp.tool()
|
| 111 |
+
def generate_sitemap(url: str, max_depth: int = 1) -> List[str]:
|
| 112 |
+
"""
|
| 113 |
+
Crawl the website to generate a simple list of internal URLs (sitemap).
|
| 114 |
+
"""
|
| 115 |
+
log_usage("mcp-seo", "generate_sitemap")
|
| 116 |
+
visited = set()
|
| 117 |
+
to_visit = [(url, 0)]
|
| 118 |
+
domain = urlparse(url).netloc
|
| 119 |
+
|
| 120 |
+
try:
|
| 121 |
+
while to_visit:
|
| 122 |
+
current_url, depth = to_visit.pop(0)
|
| 123 |
+
if current_url in visited or depth > max_depth:
|
| 124 |
+
continue
|
| 125 |
+
|
| 126 |
+
visited.add(current_url)
|
| 127 |
+
|
| 128 |
+
try:
|
| 129 |
+
response = requests.get(current_url, timeout=5)
|
| 130 |
+
if response.status_code != 200:
|
| 131 |
+
continue
|
| 132 |
+
|
| 133 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 134 |
+
links = soup.find_all('a', href=True)
|
| 135 |
+
|
| 136 |
+
for link in links:
|
| 137 |
+
href = link['href']
|
| 138 |
+
full_url = urljoin(current_url, href)
|
| 139 |
+
parsed = urlparse(full_url)
|
| 140 |
+
|
| 141 |
+
if parsed.netloc == domain and full_url not in visited:
|
| 142 |
+
# Only add html pages usually, but for simplicity we add all internal
|
| 143 |
+
to_visit.append((full_url, depth + 1))
|
| 144 |
+
except Exception:
|
| 145 |
+
continue
|
| 146 |
+
|
| 147 |
+
return sorted(list(visited))
|
| 148 |
+
except Exception as e:
|
| 149 |
+
return [f"Error: {str(e)}"]
|
| 150 |
+
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
import os
|
| 153 |
+
if os.environ.get("MCP_TRANSPORT") == "sse":
|
| 154 |
+
import uvicorn
|
| 155 |
+
port = int(os.environ.get("PORT", 7860))
|
| 156 |
+
uvicorn.run(mcp.sse_app(), host="0.0.0.0", port=port)
|
| 157 |
+
else:
|
| 158 |
+
mcp.run()
|
src/mcp-trader/Dockerfile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# Use an official Python runtime as a parent image
|
| 3 |
+
FROM python:3.12-slim
|
| 4 |
+
|
| 5 |
+
# Set the working directory in the container
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# Install system dependencies
|
| 9 |
+
# git is often needed for pip installing from git
|
| 10 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 11 |
+
git \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# Copy configuration files
|
| 15 |
+
COPY pyproject.toml .
|
| 16 |
+
|
| 17 |
+
# Install dependencies using pip
|
| 18 |
+
RUN pip install --no-cache-dir .
|
| 19 |
+
|
| 20 |
+
# Copy the specific server source code
|
| 21 |
+
COPY src/mcp-trader ./src/mcp-trader
|
| 22 |
+
COPY src/core ./src/core
|
| 23 |
+
|
| 24 |
+
# Set PYTHONPATH to include src so imports work
|
| 25 |
+
ENV PYTHONPATH=/app/src
|
| 26 |
+
|
| 27 |
+
# Expose the port that Hugging Face Spaces expects (7860)
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
ENV MCP_TRANSPORT=sse
|
| 31 |
+
|
| 32 |
+
# Run the server
|
| 33 |
+
CMD ["python", "src/mcp-trader/server.py"]
|
src/mcp-trader/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
---
|
| 3 |
+
title: MCP Trader
|
| 4 |
+
emoji: 📈
|
| 5 |
+
colorFrom: green
|
| 6 |
+
colorTo: blue
|
| 7 |
+
sdk: docker
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# MCP Trader Server
|
| 12 |
+
|
| 13 |
+
This is a Model Context Protocol (MCP) server for quantitative trading strategies and market analysis.
|
| 14 |
+
|
| 15 |
+
## Tools
|
| 16 |
+
- `get_stock_price`: Real-time stock data.
|
| 17 |
+
- `get_technical_summary`: RSI, MACD, SMA summary.
|
| 18 |
+
- `get_momentum_strategy`: Momentum-based analysis.
|
| 19 |
+
- `get_mean_reversion_strategy`: Bollinger Band strategy.
|
| 20 |
+
|
| 21 |
+
## Running Locally
|
| 22 |
+
```bash
|
| 23 |
+
python src/mcp-trader/server.py
|
| 24 |
+
```
|
src/mcp-trader/__init__.py
ADDED
|
File without changes
|
src/mcp-trader/config.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
Configuration for MCP Trader Server
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
YAHOO_FINANCE_BASE_URL = "https://query1.finance.yahoo.com/v8/finance/chart/"
|
| 11 |
+
HEADERS = {
|
| 12 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
| 13 |
+
}
|
src/mcp-trader/data/__init__.py
ADDED
|
File without changes
|
src/mcp-trader/data/fundamentals.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import yfinance as yf
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
|
| 5 |
+
def get_fundamental_data(symbol: str) -> Dict[str, Any]:
|
| 6 |
+
"""
|
| 7 |
+
Get key fundamental metrics.
|
| 8 |
+
"""
|
| 9 |
+
try:
|
| 10 |
+
ticker = yf.Ticker(symbol)
|
| 11 |
+
info = ticker.info
|
| 12 |
+
|
| 13 |
+
return {
|
| 14 |
+
"symbol": symbol,
|
| 15 |
+
"market_cap": info.get("marketCap"),
|
| 16 |
+
"pe_ratio": info.get("trailingPE"),
|
| 17 |
+
"forward_pe": info.get("forwardPE"),
|
| 18 |
+
"beta": info.get("beta"),
|
| 19 |
+
"dividend_yield": info.get("dividendYield"),
|
| 20 |
+
"sector": info.get("sector"),
|
| 21 |
+
"industry": info.get("industry")
|
| 22 |
+
}
|
| 23 |
+
except Exception as e:
|
| 24 |
+
return {"error": str(e)}
|
src/mcp-trader/data/market_data.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import yfinance as yf
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from typing import List, Dict, Union
|
| 5 |
+
|
| 6 |
+
# Try relative import first (for package mode), then absolute (for script/test mode)
|
| 7 |
+
try:
|
| 8 |
+
from ..schemas import OHLC, StockData
|
| 9 |
+
except (ImportError, ValueError):
|
| 10 |
+
try:
|
| 11 |
+
from schemas import OHLC, StockData
|
| 12 |
+
except (ImportError, ValueError):
|
| 13 |
+
# Fallback if both fail (e.g. running from root but parent not package)
|
| 14 |
+
# This shouldn't be needed if path is correct
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
def get_market_data(symbol: str, period: str = "1mo", interval: str = "1d") -> List[Dict]:
|
| 18 |
+
"""
|
| 19 |
+
Fetch historical data for a symbol.
|
| 20 |
+
"""
|
| 21 |
+
try:
|
| 22 |
+
ticker = yf.Ticker(symbol)
|
| 23 |
+
history = ticker.history(period=period, interval=interval)
|
| 24 |
+
|
| 25 |
+
if history.empty:
|
| 26 |
+
return []
|
| 27 |
+
|
| 28 |
+
data = []
|
| 29 |
+
for index, row in history.iterrows():
|
| 30 |
+
data.append({
|
| 31 |
+
"date": index.strftime("%Y-%m-%d"),
|
| 32 |
+
"open": float(row["Open"]),
|
| 33 |
+
"high": float(row["High"]),
|
| 34 |
+
"low": float(row["Low"]),
|
| 35 |
+
"close": float(row["Close"]),
|
| 36 |
+
"volume": int(row["Volume"])
|
| 37 |
+
})
|
| 38 |
+
return data
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f"Error fetching data for {symbol}: {e}")
|
| 41 |
+
return []
|
| 42 |
+
|
| 43 |
+
def get_current_price(symbol: str) -> float:
|
| 44 |
+
"""Get the latest price."""
|
| 45 |
+
try:
|
| 46 |
+
ticker = yf.Ticker(symbol)
|
| 47 |
+
info = ticker.history(period="1d")
|
| 48 |
+
if not info.empty:
|
| 49 |
+
return float(info["Close"].iloc[-1])
|
| 50 |
+
return 0.0
|
| 51 |
+
except Exception:
|
| 52 |
+
return 0.0
|
src/mcp-trader/indicators/__init__.py
ADDED
|
File without changes
|
src/mcp-trader/indicators/technical.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
from typing import Dict, Any
|
| 5 |
+
|
| 6 |
+
def calculate_sma(data: pd.DataFrame, window: int = 20) -> float:
|
| 7 |
+
return data['close'].rolling(window=window).mean().iloc[-1]
|
| 8 |
+
|
| 9 |
+
def calculate_ema(data: pd.DataFrame, window: int = 20) -> float:
|
| 10 |
+
return data['close'].ewm(span=window, adjust=False).mean().iloc[-1]
|
| 11 |
+
|
| 12 |
+
def calculate_rsi(data: pd.DataFrame, window: int = 14) -> float:
|
| 13 |
+
delta = data['close'].diff()
|
| 14 |
+
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
|
| 15 |
+
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
|
| 16 |
+
|
| 17 |
+
rs = gain / loss
|
| 18 |
+
rsi = 100 - (100 / (1 + rs))
|
| 19 |
+
return rsi.iloc[-1]
|
| 20 |
+
|
| 21 |
+
def calculate_macd(data: pd.DataFrame) -> Dict[str, float]:
|
| 22 |
+
exp1 = data['close'].ewm(span=12, adjust=False).mean()
|
| 23 |
+
exp2 = data['close'].ewm(span=26, adjust=False).mean()
|
| 24 |
+
macd = exp1 - exp2
|
| 25 |
+
signal = macd.ewm(span=9, adjust=False).mean()
|
| 26 |
+
|
| 27 |
+
return {
|
| 28 |
+
"macd": macd.iloc[-1],
|
| 29 |
+
"signal": signal.iloc[-1],
|
| 30 |
+
"histogram": macd.iloc[-1] - signal.iloc[-1]
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
def calculate_bollinger_bands(data: pd.DataFrame, window: int = 20) -> Dict[str, float]:
|
| 34 |
+
sma = data['close'].rolling(window=window).mean()
|
| 35 |
+
std = data['close'].rolling(window=window).std()
|
| 36 |
+
upper_band = sma + (std * 2)
|
| 37 |
+
lower_band = sma - (std * 2)
|
| 38 |
+
|
| 39 |
+
return {
|
| 40 |
+
"upper": upper_band.iloc[-1],
|
| 41 |
+
"middle": sma.iloc[-1],
|
| 42 |
+
"lower": lower_band.iloc[-1]
|
| 43 |
+
}
|
src/mcp-trader/schemas.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
from typing import List, Optional, Dict
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
|
| 6 |
+
class OHLC(BaseModel):
|
| 7 |
+
date: str
|
| 8 |
+
open: float
|
| 9 |
+
high: float
|
| 10 |
+
low: float
|
| 11 |
+
close: float
|
| 12 |
+
volume: int
|
| 13 |
+
|
| 14 |
+
class StockData(BaseModel):
|
| 15 |
+
symbol: str
|
| 16 |
+
interval: str
|
| 17 |
+
data: List[OHLC]
|
| 18 |
+
|
| 19 |
+
class IndicatorRequest(BaseModel):
|
| 20 |
+
symbol: str
|
| 21 |
+
interval: str = "1d"
|
| 22 |
+
period: int = 14
|
| 23 |
+
|
| 24 |
+
class IndicatorResponse(BaseModel):
|
| 25 |
+
symbol: str
|
| 26 |
+
indicator: str
|
| 27 |
+
value: float
|
| 28 |
+
signal: str # BUY, SELL, NEUTRAL
|
| 29 |
+
|
| 30 |
+
class StrategyResult(BaseModel):
|
| 31 |
+
strategy: str
|
| 32 |
+
symbol: str
|
| 33 |
+
action: str # BUY, SELL, HOLD
|
| 34 |
+
confidence: float
|
| 35 |
+
reasoning: str
|
src/mcp-trader/server.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
MCP Trader Server using FastMCP
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# Add src to pythonpath so imports work
|
| 9 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 10 |
+
src_dir = os.path.dirname(os.path.dirname(current_dir))
|
| 11 |
+
if src_dir not in sys.path:
|
| 12 |
+
sys.path.append(src_dir)
|
| 13 |
+
|
| 14 |
+
from mcp.server.fastmcp import FastMCP
|
| 15 |
+
from typing import List, Dict, Any
|
| 16 |
+
from core.mcp_telemetry import log_usage
|
| 17 |
+
|
| 18 |
+
# Local imports (assuming src/mcp-trader is a package or run from src)
|
| 19 |
+
try:
|
| 20 |
+
from .data.market_data import get_market_data, get_current_price
|
| 21 |
+
from .data.fundamentals import get_fundamental_data
|
| 22 |
+
from .strategies.momentum import analyze_momentum
|
| 23 |
+
from .strategies.mean_reversion import analyze_mean_reversion
|
| 24 |
+
from .strategies.value import analyze_value
|
| 25 |
+
from .strategies.golden_cross import analyze_golden_cross
|
| 26 |
+
from .strategies.macd_crossover import analyze_macd_crossover
|
| 27 |
+
from .strategies.bollinger_squeeze import analyze_bollinger_squeeze
|
| 28 |
+
from .indicators.technical import calculate_sma, calculate_rsi, calculate_macd
|
| 29 |
+
except ImportError:
|
| 30 |
+
# Fallback if run directly and relative imports fail
|
| 31 |
+
from data.market_data import get_market_data, get_current_price
|
| 32 |
+
from data.fundamentals import get_fundamental_data
|
| 33 |
+
from strategies.momentum import analyze_momentum
|
| 34 |
+
from strategies.mean_reversion import analyze_mean_reversion
|
| 35 |
+
from strategies.value import analyze_value
|
| 36 |
+
from strategies.golden_cross import analyze_golden_cross
|
| 37 |
+
from strategies.macd_crossover import analyze_macd_crossover
|
| 38 |
+
from strategies.bollinger_squeeze import analyze_bollinger_squeeze
|
| 39 |
+
from indicators.technical import calculate_sma, calculate_rsi, calculate_macd
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# Initialize FastMCP Server
|
| 43 |
+
mcp = FastMCP("MCP Trader", host="0.0.0.0")
|
| 44 |
+
|
| 45 |
+
@mcp.tool()
|
| 46 |
+
def get_stock_price(symbol: str) -> float:
|
| 47 |
+
"""Get the current price for a stock symbol."""
|
| 48 |
+
log_usage("mcp-trader", "get_stock_price")
|
| 49 |
+
return get_current_price(symbol)
|
| 50 |
+
|
| 51 |
+
@mcp.tool()
|
| 52 |
+
def get_stock_fundamentals(symbol: str) -> Dict[str, Any]:
|
| 53 |
+
"""Get fundamental data (PE, Market Cap, Sector) for a stock."""
|
| 54 |
+
log_usage("mcp-trader", "get_stock_fundamentals")
|
| 55 |
+
return get_fundamental_data(symbol)
|
| 56 |
+
|
| 57 |
+
@mcp.tool()
|
| 58 |
+
def get_momentum_strategy(symbol: str) -> Dict[str, Any]:
|
| 59 |
+
"""
|
| 60 |
+
Run Momentum Strategy analysis on a stock.
|
| 61 |
+
Returns Buy/Sell/Hold recommendation based on RSI, MACD, and Price Trend.
|
| 62 |
+
"""
|
| 63 |
+
log_usage("mcp-trader", "get_momentum_strategy")
|
| 64 |
+
return analyze_momentum(symbol)
|
| 65 |
+
|
| 66 |
+
@mcp.tool()
|
| 67 |
+
def get_mean_reversion_strategy(symbol: str) -> Dict[str, Any]:
|
| 68 |
+
"""
|
| 69 |
+
Run Mean Reversion Strategy analysis on a stock.
|
| 70 |
+
Returns Buy/Sell/Hold recommendation based on Bollinger Bands and RSI.
|
| 71 |
+
"""
|
| 72 |
+
return analyze_mean_reversion(symbol)
|
| 73 |
+
|
| 74 |
+
@mcp.tool()
|
| 75 |
+
def get_value_strategy(symbol: str) -> Dict[str, Any]:
|
| 76 |
+
"""
|
| 77 |
+
Run Value Strategy analysis on a stock.
|
| 78 |
+
Returns Buy/Sell/Hold recommendation based on fundamentals (PE, Dividend Yield).
|
| 79 |
+
"""
|
| 80 |
+
return analyze_value(symbol)
|
| 81 |
+
|
| 82 |
+
@mcp.tool()
|
| 83 |
+
def get_golden_cross_strategy(symbol: str) -> Dict[str, Any]:
|
| 84 |
+
"""
|
| 85 |
+
Run Golden Cross Strategy (Trend Following).
|
| 86 |
+
Detects SMA 50 crossing above/below SMA 200.
|
| 87 |
+
"""
|
| 88 |
+
return analyze_golden_cross(symbol)
|
| 89 |
+
|
| 90 |
+
@mcp.tool()
|
| 91 |
+
def get_macd_crossover_strategy(symbol: str) -> Dict[str, Any]:
|
| 92 |
+
"""
|
| 93 |
+
Run MACD Crossover Strategy (Momentum).
|
| 94 |
+
Detects MACD line crossing Signal line.
|
| 95 |
+
"""
|
| 96 |
+
return analyze_macd_crossover(symbol)
|
| 97 |
+
|
| 98 |
+
@mcp.tool()
|
| 99 |
+
def get_bollinger_squeeze_strategy(symbol: str) -> Dict[str, Any]:
|
| 100 |
+
"""
|
| 101 |
+
Run Bollinger Squeeze Strategy (Volatility).
|
| 102 |
+
Detects low volatility periods followed by potential breakouts.
|
| 103 |
+
"""
|
| 104 |
+
return analyze_bollinger_squeeze(symbol)
|
| 105 |
+
|
| 106 |
+
@mcp.tool()
|
| 107 |
+
def get_technical_summary(symbol: str) -> Dict[str, Any]:
|
| 108 |
+
"""
|
| 109 |
+
Get a summary of technical indicators for a stock (RSI, MACD, SMA).
|
| 110 |
+
"""
|
| 111 |
+
raw_data = get_market_data(symbol, period="3mo")
|
| 112 |
+
if not raw_data:
|
| 113 |
+
return {"error": "No data found"}
|
| 114 |
+
|
| 115 |
+
import pandas as pd
|
| 116 |
+
df = pd.DataFrame(raw_data)
|
| 117 |
+
|
| 118 |
+
return {
|
| 119 |
+
"symbol": symbol,
|
| 120 |
+
"price": df['close'].iloc[-1],
|
| 121 |
+
"rsi_14": calculate_rsi(df),
|
| 122 |
+
"sma_20": calculate_sma(df, 20),
|
| 123 |
+
"sma_50": calculate_sma(df, 50),
|
| 124 |
+
"macd": calculate_macd(df)
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
if __name__ == "__main__":
|
| 128 |
+
# Run the MCP server
|
| 129 |
+
import os
|
| 130 |
+
if os.environ.get("MCP_TRANSPORT") == "sse":
|
| 131 |
+
import uvicorn
|
| 132 |
+
port = int(os.environ.get("PORT", 7860))
|
| 133 |
+
uvicorn.run(mcp.sse_app(), host="0.0.0.0", port=port)
|
| 134 |
+
else:
|
| 135 |
+
mcp.run()
|
src/mcp-trader/strategies/__init__.py
ADDED
|
File without changes
|
src/mcp-trader/strategies/bollinger_squeeze.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
try:
|
| 3 |
+
from ..data.market_data import get_market_data
|
| 4 |
+
except (ImportError, ValueError):
|
| 5 |
+
from data.market_data import get_market_data
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
def analyze_bollinger_squeeze(symbol: str) -> dict:
|
| 11 |
+
"""
|
| 12 |
+
Analyze BB Squeeze (Low Volatility) + Direction.
|
| 13 |
+
"""
|
| 14 |
+
try:
|
| 15 |
+
raw_data = get_market_data(symbol, period="6mo")
|
| 16 |
+
if not raw_data or len(raw_data) < 50:
|
| 17 |
+
return {"action": "HOLD", "confidence": 0.0, "reasoning": "Insufficient data"}
|
| 18 |
+
|
| 19 |
+
df = pd.DataFrame(raw_data)
|
| 20 |
+
|
| 21 |
+
# Calculate Bands
|
| 22 |
+
sma = df['close'].rolling(window=20).mean()
|
| 23 |
+
std = df['close'].rolling(window=20).std()
|
| 24 |
+
|
| 25 |
+
upper = sma + (2 * std)
|
| 26 |
+
lower = sma - (2 * std)
|
| 27 |
+
|
| 28 |
+
# Band Width relative to price
|
| 29 |
+
df['bandwidth'] = (upper - lower) / sma
|
| 30 |
+
|
| 31 |
+
# Recent Band Width percentile (last 6 months)
|
| 32 |
+
current_bw = df['bandwidth'].iloc[-1]
|
| 33 |
+
bw_rank = df['bandwidth'].rank(pct=True).iloc[-1]
|
| 34 |
+
|
| 35 |
+
# Squeeze Condition: Width in lowest 20% of last 6mo
|
| 36 |
+
squeeze_on = bw_rank <= 0.20
|
| 37 |
+
|
| 38 |
+
# Momentum direction (RSI)
|
| 39 |
+
delta = df['close'].diff()
|
| 40 |
+
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
|
| 41 |
+
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
|
| 42 |
+
rs = gain / loss
|
| 43 |
+
rsi = 100 - (100 / (1 + rs))
|
| 44 |
+
current_rsi = rsi.iloc[-1]
|
| 45 |
+
|
| 46 |
+
action = "HOLD"
|
| 47 |
+
confidence = 0.0
|
| 48 |
+
reasoning = []
|
| 49 |
+
|
| 50 |
+
if squeeze_on:
|
| 51 |
+
reasoning.append(f"Volatility Squeeze ACTIVE (Rank: {bw_rank:.0%})")
|
| 52 |
+
if current_rsi > 50:
|
| 53 |
+
action = "BUY"
|
| 54 |
+
confidence = 0.7 # Breakout potential upwards
|
| 55 |
+
reasoning.append("Squeeze + Bullish Momentum (RSI > 50)")
|
| 56 |
+
else:
|
| 57 |
+
action = "SELL"
|
| 58 |
+
confidence = 0.7 # Breakout potential downwards
|
| 59 |
+
reasoning.append("Squeeze + Bearish Momentum (RSI < 50)")
|
| 60 |
+
else:
|
| 61 |
+
reasoning.append(f"No Squeeze (Rank: {bw_rank:.0%})")
|
| 62 |
+
|
| 63 |
+
return {
|
| 64 |
+
"strategy": "Bollinger Squeeze",
|
| 65 |
+
"symbol": symbol,
|
| 66 |
+
"action": action,
|
| 67 |
+
"confidence": confidence,
|
| 68 |
+
"reasoning": "; ".join(reasoning)
|
| 69 |
+
}
|
| 70 |
+
except Exception as e:
|
| 71 |
+
return {"action": "HOLD", "confidence": 0.0, "reasoning": f"Error: {str(e)}"}
|
src/mcp-trader/strategies/golden_cross.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
try:
|
| 3 |
+
from ..data.market_data import get_market_data
|
| 4 |
+
except (ImportError, ValueError):
|
| 5 |
+
from data.market_data import get_market_data
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
|
| 9 |
+
def analyze_golden_cross(symbol: str) -> dict:
|
| 10 |
+
"""
|
| 11 |
+
Analyze Golden Cross (SMA 50 crosses above SMA 200) strategy.
|
| 12 |
+
"""
|
| 13 |
+
# Need enough data for 200 SMA -> ~1 year (252 trading days) + buffer
|
| 14 |
+
raw_data = get_market_data(symbol, period="2y")
|
| 15 |
+
if not raw_data or len(raw_data) < 200:
|
| 16 |
+
return {"action": "HOLD", "confidence": 0.0, "reasoning": "Insufficient data for 200 SMA"}
|
| 17 |
+
|
| 18 |
+
df = pd.DataFrame(raw_data)
|
| 19 |
+
|
| 20 |
+
df['sma_50'] = df['close'].rolling(window=50).mean()
|
| 21 |
+
df['sma_200'] = df['close'].rolling(window=200).mean()
|
| 22 |
+
|
| 23 |
+
current_50 = df['sma_50'].iloc[-1]
|
| 24 |
+
current_200 = df['sma_200'].iloc[-1]
|
| 25 |
+
|
| 26 |
+
prev_50 = df['sma_50'].iloc[-2]
|
| 27 |
+
prev_200 = df['sma_200'].iloc[-2]
|
| 28 |
+
|
| 29 |
+
# Check for crossover
|
| 30 |
+
# Golden Cross: 50 crosses above 200
|
| 31 |
+
golden_cross = (prev_50 <= prev_200) and (current_50 > current_200)
|
| 32 |
+
|
| 33 |
+
# Death Cross: 50 crosses below 200
|
| 34 |
+
death_cross = (prev_50 >= prev_200) and (current_50 < current_200)
|
| 35 |
+
|
| 36 |
+
# Trend Context
|
| 37 |
+
bullish_trend = current_50 > current_200
|
| 38 |
+
|
| 39 |
+
action = "HOLD"
|
| 40 |
+
confidence = 0.0
|
| 41 |
+
reasoning = []
|
| 42 |
+
|
| 43 |
+
if golden_cross:
|
| 44 |
+
action = "BUY"
|
| 45 |
+
confidence = 0.9
|
| 46 |
+
reasoning.append("Golden Cross Detected (50 SMA crossed above 200 SMA)")
|
| 47 |
+
elif death_cross:
|
| 48 |
+
action = "SELL"
|
| 49 |
+
confidence = 0.9
|
| 50 |
+
reasoning.append("Death Cross Detected (50 SMA crossed below 200 SMA)")
|
| 51 |
+
elif bullish_trend:
|
| 52 |
+
action = "BUY"
|
| 53 |
+
confidence = 0.5
|
| 54 |
+
reasoning.append("Bullish Trend (50 SMA > 200 SMA)")
|
| 55 |
+
else:
|
| 56 |
+
action = "SELL"
|
| 57 |
+
confidence = 0.5
|
| 58 |
+
reasoning.append("Bearish Trend (50 SMA < 200 SMA)")
|
| 59 |
+
|
| 60 |
+
return {
|
| 61 |
+
"strategy": "Golden Cross",
|
| 62 |
+
"symbol": symbol,
|
| 63 |
+
"action": action,
|
| 64 |
+
"confidence": confidence,
|
| 65 |
+
"reasoning": "; ".join(reasoning)
|
| 66 |
+
}
|
src/mcp-trader/strategies/macd_crossover.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
try:
|
| 3 |
+
from ..data.market_data import get_market_data
|
| 4 |
+
except (ImportError, ValueError):
|
| 5 |
+
from data.market_data import get_market_data
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
|
| 9 |
+
def analyze_macd_crossover(symbol: str) -> dict:
|
| 10 |
+
"""
|
| 11 |
+
Analyze MACD Crossover (MACD line crosses Signal line) strategy.
|
| 12 |
+
"""
|
| 13 |
+
raw_data = get_market_data(symbol, period="6mo")
|
| 14 |
+
if not raw_data or len(raw_data) < 50:
|
| 15 |
+
return {"action": "HOLD", "confidence": 0.0, "reasoning": "Insufficient data"}
|
| 16 |
+
|
| 17 |
+
df = pd.DataFrame(raw_data)
|
| 18 |
+
|
| 19 |
+
# Calculate MACD
|
| 20 |
+
exp1 = df['close'].ewm(span=12, adjust=False).mean()
|
| 21 |
+
exp2 = df['close'].ewm(span=26, adjust=False).mean()
|
| 22 |
+
macd = exp1 - exp2
|
| 23 |
+
signal = macd.ewm(span=9, adjust=False).mean()
|
| 24 |
+
hist = macd - signal
|
| 25 |
+
|
| 26 |
+
curr_h = hist.iloc[-1]
|
| 27 |
+
prev_h = hist.iloc[-2]
|
| 28 |
+
|
| 29 |
+
# Cross Up (Bullish): Histogram goes from negative to positive
|
| 30 |
+
cross_up = (prev_h <= 0) and (curr_h > 0)
|
| 31 |
+
|
| 32 |
+
# Cross Down (Bearish): Histogram goes from positive to negative
|
| 33 |
+
cross_down = (prev_h >= 0) and (curr_h < 0)
|
| 34 |
+
|
| 35 |
+
action = "HOLD"
|
| 36 |
+
confidence = 0.0
|
| 37 |
+
reasoning = []
|
| 38 |
+
|
| 39 |
+
if cross_up:
|
| 40 |
+
action = "BUY"
|
| 41 |
+
confidence = 0.8
|
| 42 |
+
reasoning.append("MACD Bullish Crossover")
|
| 43 |
+
elif cross_down:
|
| 44 |
+
action = "SELL"
|
| 45 |
+
confidence = 0.8
|
| 46 |
+
reasoning.append("MACD Bearish Crossover")
|
| 47 |
+
elif curr_h > 0:
|
| 48 |
+
action = "BUY"
|
| 49 |
+
confidence = 0.4
|
| 50 |
+
reasoning.append("MACD Bullish Momentum (Above Signal)")
|
| 51 |
+
else:
|
| 52 |
+
action = "SELL"
|
| 53 |
+
confidence = 0.4
|
| 54 |
+
reasoning.append("MACD Bearish Momentum (Below Signal)")
|
| 55 |
+
|
| 56 |
+
return {
|
| 57 |
+
"strategy": "MACD Crossover",
|
| 58 |
+
"symbol": symbol,
|
| 59 |
+
"action": action,
|
| 60 |
+
"confidence": confidence,
|
| 61 |
+
"reasoning": "; ".join(reasoning)
|
| 62 |
+
}
|
src/mcp-trader/strategies/mean_reversion.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
try:
|
| 3 |
+
from ..data.market_data import get_market_data
|
| 4 |
+
from ..indicators.technical import calculate_bollinger_bands, calculate_rsi
|
| 5 |
+
except (ImportError, ValueError):
|
| 6 |
+
from data.market_data import get_market_data
|
| 7 |
+
from indicators.technical import calculate_bollinger_bands, calculate_rsi
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
def analyze_mean_reversion(symbol: str) -> dict:
|
| 11 |
+
"""
|
| 12 |
+
Analyze mean reversion potential.
|
| 13 |
+
"""
|
| 14 |
+
raw_data = get_market_data(symbol, period="3mo")
|
| 15 |
+
if not raw_data:
|
| 16 |
+
return {"action": "HOLD", "confidence": 0.0, "reasoning": "No data found"}
|
| 17 |
+
|
| 18 |
+
df = pd.DataFrame(raw_data)
|
| 19 |
+
|
| 20 |
+
bb = calculate_bollinger_bands(df)
|
| 21 |
+
rsi = calculate_rsi(df)
|
| 22 |
+
current_price = df['close'].iloc[-1]
|
| 23 |
+
|
| 24 |
+
score = 0
|
| 25 |
+
reasons = []
|
| 26 |
+
|
| 27 |
+
# Bollinger Bands Logic
|
| 28 |
+
if current_price < bb["lower"]:
|
| 29 |
+
score += 2
|
| 30 |
+
reasons.append(f"Price below lower BB ({bb['lower']:.2f})")
|
| 31 |
+
elif current_price > bb["upper"]:
|
| 32 |
+
score -= 2
|
| 33 |
+
reasons.append(f"Price above upper BB ({bb['upper']:.2f})")
|
| 34 |
+
else:
|
| 35 |
+
reasons.append("Price within bands")
|
| 36 |
+
|
| 37 |
+
# RSI Confirmation
|
| 38 |
+
if rsi < 30 and score > 0:
|
| 39 |
+
score += 1
|
| 40 |
+
reasons.append("RSI confirms oversold")
|
| 41 |
+
elif rsi > 70 and score < 0:
|
| 42 |
+
score -= 1
|
| 43 |
+
reasons.append("RSI confirms overbought")
|
| 44 |
+
|
| 45 |
+
action = "HOLD"
|
| 46 |
+
if score >= 2:
|
| 47 |
+
action = "BUY"
|
| 48 |
+
elif score <= -2:
|
| 49 |
+
action = "SELL"
|
| 50 |
+
|
| 51 |
+
return {
|
| 52 |
+
"strategy": "Mean Reversion",
|
| 53 |
+
"symbol": symbol,
|
| 54 |
+
"action": action,
|
| 55 |
+
"confidence": min(abs(score) / 3.0, 1.0),
|
| 56 |
+
"reasoning": "; ".join(reasons)
|
| 57 |
+
}
|
src/mcp-trader/strategies/momentum.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
try:
|
| 3 |
+
from ..data.market_data import get_market_data
|
| 4 |
+
from ..indicators.technical import calculate_rsi, calculate_macd, calculate_sma
|
| 5 |
+
except (ImportError, ValueError):
|
| 6 |
+
from data.market_data import get_market_data
|
| 7 |
+
from indicators.technical import calculate_rsi, calculate_macd, calculate_sma
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
def analyze_momentum(symbol: str) -> dict:
|
| 11 |
+
"""
|
| 12 |
+
Analyze momentum for a given symbol.
|
| 13 |
+
Returns: StrategyResult dict
|
| 14 |
+
"""
|
| 15 |
+
raw_data = get_market_data(symbol, period="3mo")
|
| 16 |
+
if not raw_data:
|
| 17 |
+
return {"action": "HOLD", "confidence": 0.0, "reasoning": "No data found"}
|
| 18 |
+
|
| 19 |
+
df = pd.DataFrame(raw_data)
|
| 20 |
+
|
| 21 |
+
rsi = calculate_rsi(df)
|
| 22 |
+
macd_data = calculate_macd(df)
|
| 23 |
+
sma_50 = calculate_sma(df, 50)
|
| 24 |
+
current_price = df['close'].iloc[-1]
|
| 25 |
+
|
| 26 |
+
score = 0
|
| 27 |
+
reasons = []
|
| 28 |
+
|
| 29 |
+
# RSI Logic
|
| 30 |
+
if rsi < 30:
|
| 31 |
+
score += 1
|
| 32 |
+
reasons.append(f"RSI is oversold ({rsi:.2f})")
|
| 33 |
+
elif rsi > 70:
|
| 34 |
+
score -= 1
|
| 35 |
+
reasons.append(f"RSI is overbought ({rsi:.2f})")
|
| 36 |
+
else:
|
| 37 |
+
reasons.append(f"RSI is neutral ({rsi:.2f})")
|
| 38 |
+
|
| 39 |
+
# MACD Logic
|
| 40 |
+
if macd_data["histogram"] > 0:
|
| 41 |
+
score += 1
|
| 42 |
+
reasons.append("MACD histogram is positive")
|
| 43 |
+
else:
|
| 44 |
+
score -= 1
|
| 45 |
+
reasons.append("MACD histogram is negative")
|
| 46 |
+
|
| 47 |
+
# Trend Logic
|
| 48 |
+
if current_price > sma_50:
|
| 49 |
+
score += 1
|
| 50 |
+
reasons.append("Price is above 50 SMA")
|
| 51 |
+
else:
|
| 52 |
+
score -= 1
|
| 53 |
+
reasons.append("Price is below 50 SMA")
|
| 54 |
+
|
| 55 |
+
# Final Decision
|
| 56 |
+
action = "HOLD"
|
| 57 |
+
if score >= 2:
|
| 58 |
+
action = "BUY"
|
| 59 |
+
elif score <= -2:
|
| 60 |
+
action = "SELL"
|
| 61 |
+
|
| 62 |
+
return {
|
| 63 |
+
"strategy": "Momentum",
|
| 64 |
+
"symbol": symbol,
|
| 65 |
+
"action": action,
|
| 66 |
+
"confidence": abs(score) / 3.0,
|
| 67 |
+
"reasoning": "; ".join(reasons)
|
| 68 |
+
}
|