Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- pyproject.toml +1 -1
- src/core/mcp_telemetry.py +220 -33
- src/mcp-azure-sre/server.py +15 -2
- src/mcp-github/server.py +15 -1
- src/mcp-hub/api.py +79 -28
- src/mcp-hub/src/App.vue +2 -2
- src/mcp-rag-secure/server.py +29 -17
- src/mcp-seo/server.py +14 -2
- src/mcp-trader/server.py +21 -2
- src/mcp-trading-research/server.py +12 -1
- src/mcp-weather/server.py +40 -15
- src/mcp-web/server.py +17 -2
pyproject.toml
CHANGED
|
@@ -159,7 +159,7 @@ dependencies = [
|
|
| 159 |
"google-auth-oauthlib>=0.4.6",
|
| 160 |
"google-auth-httplib2>=0.1.0",
|
| 161 |
"autoflake>=1.5.0",
|
| 162 |
-
|
| 163 |
]
|
| 164 |
|
| 165 |
[dependency-groups]
|
|
|
|
| 159 |
"google-auth-oauthlib>=0.4.6",
|
| 160 |
"google-auth-httplib2>=0.1.0",
|
| 161 |
"autoflake>=1.5.0",
|
| 162 |
+
"psycopg2-binary>=2.9.9",
|
| 163 |
]
|
| 164 |
|
| 165 |
[dependency-groups]
|
src/core/mcp_telemetry.py
CHANGED
|
@@ -10,8 +10,9 @@ from pathlib import Path
|
|
| 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:
|
|
@@ -19,6 +20,22 @@ else:
|
|
| 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()
|
|
@@ -27,6 +44,57 @@ def _get_conn():
|
|
| 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
|
|
@@ -46,7 +114,34 @@ def _init_db():
|
|
| 46 |
tool TEXT NOT NULL
|
| 47 |
)
|
| 48 |
""")
|
| 49 |
-
conn.execute("CREATE INDEX IF NOT EXISTS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
conn.close()
|
| 51 |
except Exception as e:
|
| 52 |
print(f"DB Init Failed: {e}")
|
|
@@ -56,45 +151,111 @@ def _init_db():
|
|
| 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 |
-
|
| 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 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
def get_metrics():
|
| 84 |
-
"""Aggregates metrics from
|
| 85 |
-
if not DB_FILE.exists():
|
|
|
|
| 86 |
return {}
|
| 87 |
|
| 88 |
try:
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
if server not in metrics:
|
| 100 |
metrics[server] = {"hourly": 0, "weekly": 0, "monthly": 0}
|
|
@@ -104,6 +265,7 @@ def get_metrics():
|
|
| 104 |
metrics[server]["hourly"] += 1
|
| 105 |
if delta.days < 7:
|
| 106 |
metrics[server]["weekly"] += 1
|
|
|
|
| 107 |
metrics[server]["monthly"] += 1
|
| 108 |
|
| 109 |
return metrics
|
|
@@ -113,7 +275,7 @@ def get_metrics():
|
|
| 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:
|
|
@@ -121,11 +283,21 @@ def get_usage_history(range_hours: int = 24, intervals: int = 12):
|
|
| 121 |
start_time = now - timedelta(hours=range_hours)
|
| 122 |
bucket_size = (range_hours * 3600) / intervals
|
| 123 |
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
@@ -135,7 +307,10 @@ def get_usage_history(range_hours: int = 24, intervals: int = 12):
|
|
| 135 |
datasets = {s: [0] * intervals for s in active_servers}
|
| 136 |
|
| 137 |
for row in rows:
|
| 138 |
-
ts =
|
|
|
|
|
|
|
|
|
|
| 139 |
delta = (ts - start_time).total_seconds()
|
| 140 |
bucket_idx = int(delta // bucket_size)
|
| 141 |
if 0 <= bucket_idx < intervals:
|
|
@@ -210,16 +385,28 @@ def get_system_metrics():
|
|
| 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 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
|
|
| 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 |
+
PG_CONN_STR = os.environ.get("MCP_TRACES_DB")
|
| 14 |
|
| 15 |
+
# Single SQLite DB for the Hub (fallback)
|
| 16 |
if os.path.exists("/app"):
|
| 17 |
DB_FILE = Path("/tmp/mcp_logs.db")
|
| 18 |
else:
|
|
|
|
| 20 |
DB_FILE = Path(__file__).parent.parent.parent / "mcp_logs.db"
|
| 21 |
|
| 22 |
def _get_conn():
|
| 23 |
+
# PostgreSQL Mode
|
| 24 |
+
if PG_CONN_STR and IS_HUB:
|
| 25 |
+
try:
|
| 26 |
+
import psycopg2
|
| 27 |
+
from psycopg2.extras import RealDictCursor
|
| 28 |
+
conn = psycopg2.connect(PG_CONN_STR)
|
| 29 |
+
# Init schema if needed (lazy check could be optimized)
|
| 30 |
+
_init_pg_db(conn)
|
| 31 |
+
return conn
|
| 32 |
+
except Exception as e:
|
| 33 |
+
print(f"Postgres Connection Failed: {e}")
|
| 34 |
+
# Fallback to SQLite not recommended if PG configured, but handling graceful failure might be needed.
|
| 35 |
+
# For now, we raise or assume SQLite fallback if PG fail? Let's error out to be safe.
|
| 36 |
+
raise e
|
| 37 |
+
|
| 38 |
+
# SQLite Mode (Default)
|
| 39 |
# Auto-init if missing (lazy creation)
|
| 40 |
if IS_HUB and not os.path.exists(DB_FILE):
|
| 41 |
_init_db()
|
|
|
|
| 44 |
conn.row_factory = sqlite3.Row
|
| 45 |
return conn
|
| 46 |
|
| 47 |
+
|
| 48 |
+
def _init_pg_db(conn):
|
| 49 |
+
"""Initializes the PostgreSQL database with required tables."""
|
| 50 |
+
try:
|
| 51 |
+
with conn.cursor() as cur:
|
| 52 |
+
# Logs
|
| 53 |
+
cur.execute("""
|
| 54 |
+
CREATE TABLE IF NOT EXISTS logs (
|
| 55 |
+
id SERIAL PRIMARY KEY,
|
| 56 |
+
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
|
| 57 |
+
server VARCHAR(255) NOT NULL,
|
| 58 |
+
tool VARCHAR(255) NOT NULL
|
| 59 |
+
)
|
| 60 |
+
""")
|
| 61 |
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(timestamp)")
|
| 62 |
+
|
| 63 |
+
# Traces
|
| 64 |
+
cur.execute("""
|
| 65 |
+
CREATE TABLE IF NOT EXISTS traces (
|
| 66 |
+
id SERIAL PRIMARY KEY,
|
| 67 |
+
trace_id VARCHAR(64) NOT NULL,
|
| 68 |
+
span_id VARCHAR(64) NOT NULL,
|
| 69 |
+
parent_id VARCHAR(64),
|
| 70 |
+
name VARCHAR(255) NOT NULL,
|
| 71 |
+
status VARCHAR(50),
|
| 72 |
+
start_time TIMESTAMP,
|
| 73 |
+
end_time TIMESTAMP,
|
| 74 |
+
duration_ms FLOAT,
|
| 75 |
+
server VARCHAR(255)
|
| 76 |
+
)
|
| 77 |
+
""")
|
| 78 |
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_traces_tid ON traces(trace_id)")
|
| 79 |
+
|
| 80 |
+
# Metrics
|
| 81 |
+
cur.execute("""
|
| 82 |
+
CREATE TABLE IF NOT EXISTS metrics (
|
| 83 |
+
id SERIAL PRIMARY KEY,
|
| 84 |
+
name VARCHAR(255) NOT NULL,
|
| 85 |
+
value FLOAT NOT NULL,
|
| 86 |
+
tags TEXT,
|
| 87 |
+
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
|
| 88 |
+
server VARCHAR(255)
|
| 89 |
+
)
|
| 90 |
+
""")
|
| 91 |
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_metrics_ts ON metrics(timestamp)")
|
| 92 |
+
|
| 93 |
+
conn.commit()
|
| 94 |
+
except Exception as e:
|
| 95 |
+
print(f"Postgres DB Init Failed: {e}")
|
| 96 |
+
conn.rollback()
|
| 97 |
+
|
| 98 |
def _init_db():
|
| 99 |
"""Initializes the SQLite database with required tables."""
|
| 100 |
# Ensure parent dir exists
|
|
|
|
| 114 |
tool TEXT NOT NULL
|
| 115 |
)
|
| 116 |
""")
|
| 117 |
+
conn.execute("CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(timestamp)")
|
| 118 |
+
|
| 119 |
+
conn.execute("""
|
| 120 |
+
CREATE TABLE IF NOT EXISTS traces (
|
| 121 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 122 |
+
trace_id TEXT NOT NULL,
|
| 123 |
+
span_id TEXT NOT NULL,
|
| 124 |
+
parent_id TEXT,
|
| 125 |
+
name TEXT NOT NULL,
|
| 126 |
+
status TEXT,
|
| 127 |
+
start_time TEXT,
|
| 128 |
+
end_time TEXT,
|
| 129 |
+
duration_ms REAL,
|
| 130 |
+
server TEXT
|
| 131 |
+
)
|
| 132 |
+
""")
|
| 133 |
+
|
| 134 |
+
conn.execute("""
|
| 135 |
+
CREATE TABLE IF NOT EXISTS metrics (
|
| 136 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 137 |
+
name TEXT NOT NULL,
|
| 138 |
+
value REAL NOT NULL,
|
| 139 |
+
tags TEXT,
|
| 140 |
+
timestamp TEXT NOT NULL,
|
| 141 |
+
server TEXT
|
| 142 |
+
)
|
| 143 |
+
""")
|
| 144 |
+
|
| 145 |
conn.close()
|
| 146 |
except Exception as e:
|
| 147 |
print(f"DB Init Failed: {e}")
|
|
|
|
| 151 |
def log_usage(server_name: str, tool_name: str):
|
| 152 |
"""Logs a usage event. Writes to DB if Hub, else POSTs to Hub API."""
|
| 153 |
timestamp = datetime.now().isoformat()
|
| 154 |
+
if IS_HUB:
|
| 155 |
+
_write_db("logs", {"timestamp": timestamp, "server": server_name, "tool": tool_name})
|
| 156 |
+
else:
|
| 157 |
+
_send_remote("log", {"timestamp": timestamp, "server": server_name, "tool": tool_name})
|
| 158 |
+
|
| 159 |
+
def log_trace(server_name: str, trace_id: str, span_id: str, name: str, duration_ms: float,
|
| 160 |
+
status: str = "ok", parent_id: str = None):
|
| 161 |
+
"""Logs a trace span."""
|
| 162 |
+
now = datetime.now()
|
| 163 |
+
end_time = now.isoformat()
|
| 164 |
+
start_time = (now - timedelta(milliseconds=duration_ms)).isoformat()
|
| 165 |
+
|
| 166 |
+
data = {
|
| 167 |
+
"server": server_name,
|
| 168 |
+
"trace_id": trace_id,
|
| 169 |
+
"span_id": span_id,
|
| 170 |
+
"parent_id": parent_id,
|
| 171 |
+
"name": name,
|
| 172 |
+
"status": status,
|
| 173 |
+
"start_time": start_time,
|
| 174 |
+
"end_time": end_time,
|
| 175 |
+
"duration_ms": duration_ms
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
if IS_HUB:
|
| 179 |
+
_write_db("traces", data)
|
| 180 |
+
else:
|
| 181 |
+
_send_remote("trace", data)
|
| 182 |
+
|
| 183 |
+
def log_metric(server_name: str, name: str, value: float, tags: dict = None):
|
| 184 |
+
"""Logs a metric point."""
|
| 185 |
+
timestamp = datetime.now().isoformat()
|
| 186 |
+
tags_str = json.dumps(tags) if tags else "{}"
|
| 187 |
+
|
| 188 |
+
data = {
|
| 189 |
+
"server": server_name,
|
| 190 |
+
"name": name,
|
| 191 |
+
"value": value,
|
| 192 |
+
"tags": tags_str,
|
| 193 |
+
"timestamp": timestamp
|
| 194 |
+
}
|
| 195 |
|
|
|
|
| 196 |
if IS_HUB:
|
| 197 |
+
_write_db("metrics", data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
else:
|
| 199 |
+
_send_remote("metric", data)
|
| 200 |
+
|
| 201 |
+
def _write_db(table: str, data: dict):
|
| 202 |
+
"""Helper to write to DB (PG or SQLite)."""
|
| 203 |
+
try:
|
| 204 |
+
conn = _get_conn()
|
| 205 |
+
cols = list(data.keys())
|
| 206 |
+
placeholders = ["%s"] * len(cols) if PG_CONN_STR and IS_HUB else ["?"] * len(cols)
|
| 207 |
+
query = f"INSERT INTO {table} ({', '.join(cols)}) VALUES ({', '.join(placeholders)})"
|
| 208 |
+
values = list(data.values())
|
| 209 |
+
|
| 210 |
+
if PG_CONN_STR and IS_HUB:
|
| 211 |
+
with conn.cursor() as cur:
|
| 212 |
+
cur.execute(query, values)
|
| 213 |
+
conn.commit()
|
| 214 |
+
conn.close()
|
| 215 |
+
else:
|
| 216 |
+
with conn:
|
| 217 |
+
conn.execute(query, values)
|
| 218 |
+
conn.close()
|
| 219 |
+
except Exception as e:
|
| 220 |
+
print(f"Local Write Failed ({table}): {e}")
|
| 221 |
+
|
| 222 |
+
def _send_remote(type: str, data: dict):
|
| 223 |
+
"""Helper to post to Hub."""
|
| 224 |
+
try:
|
| 225 |
+
# Fire and forget
|
| 226 |
+
requests.post(f"{HUB_URL}/api/telemetry/{type}", json=data, timeout=2)
|
| 227 |
+
except Exception:
|
| 228 |
+
pass
|
| 229 |
|
| 230 |
def get_metrics():
|
| 231 |
+
"""Aggregates metrics from DB."""
|
| 232 |
+
if not IS_HUB and not DB_FILE.exists():
|
| 233 |
+
# If not Hub and no local sqlite, nothing to show
|
| 234 |
return {}
|
| 235 |
|
| 236 |
try:
|
| 237 |
+
conn = _get_conn()
|
| 238 |
+
metrics = {}
|
| 239 |
+
rows = []
|
| 240 |
+
|
| 241 |
+
if PG_CONN_STR and IS_HUB:
|
| 242 |
+
from psycopg2.extras import RealDictCursor
|
| 243 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 244 |
+
cur.execute("SELECT server, timestamp FROM logs")
|
| 245 |
+
rows = cur.fetchall()
|
| 246 |
+
conn.close()
|
| 247 |
+
else:
|
| 248 |
rows = conn.execute("SELECT server, timestamp FROM logs").fetchall()
|
| 249 |
+
conn.close()
|
| 250 |
|
| 251 |
now = datetime.now()
|
|
|
|
| 252 |
|
| 253 |
for row in rows:
|
| 254 |
server = row["server"]
|
| 255 |
+
# Handle different timestamp formats (PG vs SQLite textual)
|
| 256 |
+
ts = row["timestamp"]
|
| 257 |
+
if isinstance(ts, str):
|
| 258 |
+
ts = datetime.fromisoformat(ts)
|
| 259 |
|
| 260 |
if server not in metrics:
|
| 261 |
metrics[server] = {"hourly": 0, "weekly": 0, "monthly": 0}
|
|
|
|
| 265 |
metrics[server]["hourly"] += 1
|
| 266 |
if delta.days < 7:
|
| 267 |
metrics[server]["weekly"] += 1
|
| 268 |
+
if delta.days < 30: # Assuming a month is roughly 30 days for simplicity
|
| 269 |
metrics[server]["monthly"] += 1
|
| 270 |
|
| 271 |
return metrics
|
|
|
|
| 275 |
|
| 276 |
def get_usage_history(range_hours: int = 24, intervals: int = 12):
|
| 277 |
"""Returns time-series data for the chart."""
|
| 278 |
+
if not IS_HUB and not DB_FILE.exists():
|
| 279 |
return _generate_mock_history(range_hours, intervals)
|
| 280 |
|
| 281 |
try:
|
|
|
|
| 283 |
start_time = now - timedelta(hours=range_hours)
|
| 284 |
bucket_size = (range_hours * 3600) / intervals
|
| 285 |
|
| 286 |
+
conn = _get_conn()
|
| 287 |
+
rows = []
|
| 288 |
+
|
| 289 |
+
if PG_CONN_STR and IS_HUB:
|
| 290 |
+
from psycopg2.extras import RealDictCursor
|
| 291 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 292 |
+
cur.execute("SELECT server, timestamp FROM logs WHERE timestamp >= %s", (start_time,))
|
| 293 |
+
rows = cur.fetchall()
|
| 294 |
+
conn.close()
|
| 295 |
+
else:
|
| 296 |
rows = conn.execute(
|
| 297 |
"SELECT server, timestamp FROM logs WHERE timestamp >= ?",
|
| 298 |
(start_time.isoformat(),)
|
| 299 |
).fetchall()
|
| 300 |
+
conn.close()
|
| 301 |
|
| 302 |
if not rows:
|
| 303 |
return _generate_mock_history(range_hours, intervals)
|
|
|
|
| 307 |
datasets = {s: [0] * intervals for s in active_servers}
|
| 308 |
|
| 309 |
for row in rows:
|
| 310 |
+
ts = row["timestamp"]
|
| 311 |
+
if isinstance(ts, str):
|
| 312 |
+
ts = datetime.fromisoformat(ts)
|
| 313 |
+
|
| 314 |
delta = (ts - start_time).total_seconds()
|
| 315 |
bucket_idx = int(delta // bucket_size)
|
| 316 |
if 0 <= bucket_idx < intervals:
|
|
|
|
| 385 |
|
| 386 |
def get_recent_logs(server_id: str, limit: int = 50):
|
| 387 |
"""Fetches the most recent logs for a specific server."""
|
| 388 |
+
if not IS_HUB and not DB_FILE.exists():
|
| 389 |
return []
|
| 390 |
|
| 391 |
try:
|
| 392 |
+
conn = _get_conn()
|
| 393 |
+
rows = []
|
| 394 |
+
|
| 395 |
+
if PG_CONN_STR and IS_HUB:
|
| 396 |
+
from psycopg2.extras import RealDictCursor
|
| 397 |
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
| 398 |
+
cur.execute(
|
| 399 |
+
"SELECT timestamp, tool FROM logs WHERE server = %s ORDER BY id DESC LIMIT %s",
|
| 400 |
+
(server_id, limit)
|
| 401 |
+
)
|
| 402 |
+
rows = cur.fetchall()
|
| 403 |
+
conn.close()
|
| 404 |
+
else:
|
| 405 |
rows = conn.execute(
|
| 406 |
"SELECT timestamp, tool FROM logs WHERE server = ? ORDER BY id DESC LIMIT ?",
|
| 407 |
(server_id, limit)
|
| 408 |
).fetchall()
|
| 409 |
+
conn.close()
|
| 410 |
|
| 411 |
return [dict(r) for r in rows]
|
| 412 |
except Exception as e:
|
src/mcp-azure-sre/server.py
CHANGED
|
@@ -14,7 +14,9 @@ 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:
|
|
@@ -45,7 +47,11 @@ def list_resources(subscription_id: str, resource_group: Optional[str] = None) -
|
|
| 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)
|
|
@@ -55,8 +61,15 @@ def list_resources(subscription_id: str, resource_group: Optional[str] = None) -
|
|
| 55 |
else:
|
| 56 |
resources = client.resources.list()
|
| 57 |
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
except Exception as e:
|
|
|
|
|
|
|
| 60 |
return [{"error": str(e)}]
|
| 61 |
|
| 62 |
@mcp.tool()
|
|
|
|
| 14 |
sys.path.append(src_dir)
|
| 15 |
|
| 16 |
from mcp.server.fastmcp import FastMCP
|
| 17 |
+
from core.mcp_telemetry import log_usage, log_trace, log_metric
|
| 18 |
+
import uuid
|
| 19 |
+
import time
|
| 20 |
|
| 21 |
# Azure Imports
|
| 22 |
try:
|
|
|
|
| 47 |
"""
|
| 48 |
List Azure resources in a subscription or resource group.
|
| 49 |
"""
|
| 50 |
+
start_time = time.time()
|
| 51 |
+
trace_id = str(uuid.uuid4())
|
| 52 |
+
span_id = str(uuid.uuid4())
|
| 53 |
log_usage("mcp-azure-sre", "list_resources")
|
| 54 |
+
|
| 55 |
try:
|
| 56 |
cred = get_credential()
|
| 57 |
client = ResourceManagementClient(cred, subscription_id)
|
|
|
|
| 61 |
else:
|
| 62 |
resources = client.resources.list()
|
| 63 |
|
| 64 |
+
results = [{"name": r.name, "type": r.type, "location": r.location, "id": r.id} for r in resources]
|
| 65 |
+
|
| 66 |
+
duration = (time.time() - start_time) * 1000
|
| 67 |
+
log_trace("mcp-azure-sre", trace_id, span_id, "list_resources", duration, "ok")
|
| 68 |
+
log_metric("mcp-azure-sre", "resources_scanned", len(results), {"sub": subscription_id})
|
| 69 |
+
return results
|
| 70 |
except Exception as e:
|
| 71 |
+
duration = (time.time() - start_time) * 1000
|
| 72 |
+
log_trace("mcp-azure-sre", trace_id, span_id, "list_resources", duration, "error")
|
| 73 |
return [{"error": str(e)}]
|
| 74 |
|
| 75 |
@mcp.tool()
|
src/mcp-github/server.py
CHANGED
|
@@ -6,7 +6,10 @@ import sys
|
|
| 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__))
|
|
@@ -39,7 +42,11 @@ def list_repositories() -> List[Dict[str, Any]]:
|
|
| 39 |
"""
|
| 40 |
List all repositories for the authenticated user/owner.
|
| 41 |
"""
|
|
|
|
|
|
|
|
|
|
| 42 |
log_usage("mcp-github", "list_repositories")
|
|
|
|
| 43 |
try:
|
| 44 |
g = get_client()
|
| 45 |
# Get repos for the owner/authenticated user
|
|
@@ -56,8 +63,15 @@ def list_repositories() -> List[Dict[str, Any]]:
|
|
| 56 |
"updated_at": str(repo.updated_at),
|
| 57 |
"language": repo.language
|
| 58 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
return results
|
|
|
|
| 60 |
except Exception as e:
|
|
|
|
|
|
|
| 61 |
return [{"error": str(e)}]
|
| 62 |
|
| 63 |
@mcp.tool()
|
|
|
|
| 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, log_trace, log_metric
|
| 10 |
+
import uuid
|
| 11 |
+
import time
|
| 12 |
+
import datetime
|
| 13 |
|
| 14 |
# Add src to pythonpath
|
| 15 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
| 42 |
"""
|
| 43 |
List all repositories for the authenticated user/owner.
|
| 44 |
"""
|
| 45 |
+
start_time = time.time()
|
| 46 |
+
trace_id = str(uuid.uuid4())
|
| 47 |
+
span_id = str(uuid.uuid4())
|
| 48 |
log_usage("mcp-github", "list_repositories")
|
| 49 |
+
|
| 50 |
try:
|
| 51 |
g = get_client()
|
| 52 |
# Get repos for the owner/authenticated user
|
|
|
|
| 63 |
"updated_at": str(repo.updated_at),
|
| 64 |
"language": repo.language
|
| 65 |
})
|
| 66 |
+
|
| 67 |
+
duration = (time.time() - start_time) * 1000
|
| 68 |
+
log_trace("mcp-github", trace_id, span_id, "list_repositories", duration, "ok")
|
| 69 |
+
log_metric("mcp-github", "repos_fetched", len(results), {"status": "ok"})
|
| 70 |
return results
|
| 71 |
+
|
| 72 |
except Exception as e:
|
| 73 |
+
duration = (time.time() - start_time) * 1000
|
| 74 |
+
log_trace("mcp-github", trace_id, span_id, "list_repositories", duration, "error")
|
| 75 |
return [{"error": str(e)}]
|
| 76 |
|
| 77 |
@mcp.tool()
|
src/mcp-hub/api.py
CHANGED
|
@@ -13,52 +13,82 @@ from datetime import datetime, timedelta
|
|
| 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
|
| 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
|
| 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
|
| 34 |
server: str
|
| 35 |
tool: str
|
| 36 |
timestamp: Optional[str] = None
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
app = FastAPI()
|
| 39 |
|
| 40 |
@app.post("/api/telemetry")
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
try:
|
| 55 |
ts = event.timestamp or datetime.now().isoformat()
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
return {"status": "ok"}
|
| 60 |
except Exception as e:
|
| 61 |
-
print(f"
|
| 62 |
return {"status": "error", "message": str(e)}
|
| 63 |
|
| 64 |
app.add_middleware(
|
|
@@ -166,11 +196,32 @@ print(result.final_text)
|
|
| 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
|
| 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):
|
|
|
|
| 13 |
# Add parent dir to path for imports
|
| 14 |
sys.path.append(str(Path(__file__).parent.parent))
|
| 15 |
|
| 16 |
+
# Core Telemetry Imports for Metrics/History
|
| 17 |
+
from core.mcp_telemetry import get_metrics, get_usage_history, get_system_metrics, get_recent_logs
|
| 18 |
+
|
| 19 |
+
|
| 20 |
# Telemetry Import
|
| 21 |
try:
|
| 22 |
+
from core.mcp_telemetry import _write_db
|
| 23 |
except ImportError:
|
| 24 |
# If standard import fails, try absolute path fallback
|
| 25 |
sys.path.append(str(Path(__file__).parent.parent.parent))
|
| 26 |
+
from src.core.mcp_telemetry import _write_db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
from pydantic import BaseModel
|
| 29 |
|
| 30 |
+
class LogEvent(BaseModel):
|
| 31 |
server: str
|
| 32 |
tool: str
|
| 33 |
timestamp: Optional[str] = None
|
| 34 |
|
| 35 |
+
class TraceEvent(BaseModel):
|
| 36 |
+
server: str
|
| 37 |
+
trace_id: str
|
| 38 |
+
span_id: str
|
| 39 |
+
name: str
|
| 40 |
+
duration_ms: float
|
| 41 |
+
status: Optional[str] = "ok"
|
| 42 |
+
parent_id: Optional[str] = None
|
| 43 |
+
start_time: Optional[str] = None
|
| 44 |
+
end_time: Optional[str] = None
|
| 45 |
+
|
| 46 |
+
class MetricEvent(BaseModel):
|
| 47 |
+
server: str
|
| 48 |
+
name: str
|
| 49 |
+
value: float
|
| 50 |
+
tags: Optional[str] = "{}"
|
| 51 |
+
timestamp: Optional[str] = None
|
| 52 |
+
|
| 53 |
app = FastAPI()
|
| 54 |
|
| 55 |
@app.post("/api/telemetry")
|
| 56 |
+
@app.post("/api/telemetry/log")
|
| 57 |
+
async def ingest_log(event: LogEvent):
|
| 58 |
+
"""Ingests usage logs."""
|
| 59 |
+
try:
|
| 60 |
+
ts = event.timestamp or datetime.now().isoformat()
|
| 61 |
+
_write_db("logs", {
|
| 62 |
+
"timestamp": ts,
|
| 63 |
+
"server": event.server,
|
| 64 |
+
"tool": event.tool
|
| 65 |
+
})
|
| 66 |
+
return {"status": "ok"}
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f"Log Ingest Failed: {e}")
|
| 69 |
+
return {"status": "error", "message": str(e)}
|
| 70 |
+
|
| 71 |
+
@app.post("/api/telemetry/trace")
|
| 72 |
+
async def ingest_trace(event: TraceEvent):
|
| 73 |
+
"""Ingests distributed traces."""
|
| 74 |
+
try:
|
| 75 |
+
_write_db("traces", event.dict())
|
| 76 |
+
return {"status": "ok"}
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"Trace Ingest Failed: {e}")
|
| 79 |
+
return {"status": "error", "message": str(e)}
|
| 80 |
+
|
| 81 |
+
@app.post("/api/telemetry/metric")
|
| 82 |
+
async def ingest_metric(event: MetricEvent):
|
| 83 |
+
"""Ingests quantitative metrics."""
|
| 84 |
try:
|
| 85 |
ts = event.timestamp or datetime.now().isoformat()
|
| 86 |
+
data = event.dict()
|
| 87 |
+
data["timestamp"] = ts
|
| 88 |
+
_write_db("metrics", data)
|
| 89 |
return {"status": "ok"}
|
| 90 |
except Exception as e:
|
| 91 |
+
print(f"Metric Ingest Failed: {e}")
|
| 92 |
return {"status": "error", "message": str(e)}
|
| 93 |
|
| 94 |
app.add_middleware(
|
|
|
|
| 196 |
|
| 197 |
@app.on_event("startup")
|
| 198 |
async def startup_event():
|
| 199 |
+
# 1. Environment Check
|
| 200 |
token = os.environ.get("HF_TOKEN")
|
| 201 |
+
is_hub = os.environ.get("MCP_IS_HUB", "false")
|
| 202 |
+
pg_db = os.environ.get("MCP_TRACES_DB")
|
| 203 |
+
|
| 204 |
+
print("--- MCP HUB BOOT SEQUENCE ---")
|
| 205 |
+
print(f"ENV MCP_IS_HUB: {is_hub}")
|
| 206 |
+
|
| 207 |
if token:
|
| 208 |
+
print(f"ENV HF_TOKEN: Configured ({len(token)} chars)")
|
| 209 |
else:
|
| 210 |
print("WARNING: HF_TOKEN not set! Live status checks will fail.")
|
| 211 |
+
|
| 212 |
+
# 2. Database Check
|
| 213 |
+
if pg_db:
|
| 214 |
+
print(f"ENV MCP_TRACES_DB: Configured (Scheme: {pg_db.split(':')[0]})")
|
| 215 |
+
try:
|
| 216 |
+
import psycopg2
|
| 217 |
+
print("DEPENDENCY: psycopg2-binary installed.")
|
| 218 |
+
# We don't connect here to avoid blocking start, but telemetry module will try.
|
| 219 |
+
except ImportError:
|
| 220 |
+
print("CRITICAL ERROR: psycopg2-binary NOT installed. Postgres will fail!")
|
| 221 |
+
else:
|
| 222 |
+
print("ENV MCP_TRACES_DB: Not set. Using SQLite fallback.")
|
| 223 |
+
|
| 224 |
+
print("--- SEQUENCE COMPLETE ---")
|
| 225 |
|
| 226 |
@app.get("/api/servers/{server_id}/logs")
|
| 227 |
async def get_server_logs(server_id: str):
|
src/mcp-hub/src/App.vue
CHANGED
|
@@ -161,7 +161,7 @@ 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('
|
| 165 |
const ranges = ['1h', '24h', '7d', '30d']
|
| 166 |
const hoverInfo = ref(null)
|
| 167 |
let logTimer = null
|
|
@@ -327,7 +327,7 @@ onMounted(() => {
|
|
| 327 |
setInterval(() => {
|
| 328 |
fetchData()
|
| 329 |
fetchUsage()
|
| 330 |
-
},
|
| 331 |
})
|
| 332 |
</script>
|
| 333 |
|
|
|
|
| 161 |
const usageData = ref({ labels: [], datasets: [] })
|
| 162 |
const selectedServer = ref(null)
|
| 163 |
const currentLogs = ref('Initializing terminal...')
|
| 164 |
+
const selectedRange = ref('1h')
|
| 165 |
const ranges = ['1h', '24h', '7d', '30d']
|
| 166 |
const hoverInfo = ref(null)
|
| 167 |
let logTimer = null
|
|
|
|
| 327 |
setInterval(() => {
|
| 328 |
fetchData()
|
| 329 |
fetchUsage()
|
| 330 |
+
}, 300000)
|
| 331 |
})
|
| 332 |
</script>
|
| 333 |
|
src/mcp-rag-secure/server.py
CHANGED
|
@@ -10,7 +10,8 @@ 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")
|
|
@@ -69,24 +70,35 @@ def query_knowledge_base(tenant_id: str, query: str, k: int = 3) -> List[Dict[st
|
|
| 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 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
@mcp.tool()
|
| 92 |
def delete_tenant_data(tenant_id: str) -> str:
|
|
|
|
| 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, log_trace, log_metric
|
| 14 |
+
import time
|
| 15 |
|
| 16 |
# Initialize FastMCP Server
|
| 17 |
mcp = FastMCP("Secure RAG", host="0.0.0.0")
|
|
|
|
| 70 |
"""
|
| 71 |
Query the knowledge base. Results are strictly filtered by tenant_id.
|
| 72 |
"""
|
| 73 |
+
start_time = time.time()
|
| 74 |
+
trace_id = str(uuid.uuid4())
|
| 75 |
+
span_id = str(uuid.uuid4())
|
| 76 |
log_usage("mcp-rag-secure", "query_knowledge_base")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
try:
|
| 79 |
+
results = collection.query(
|
| 80 |
+
query_texts=[query],
|
| 81 |
+
n_results=k,
|
| 82 |
+
where={"tenant_id": tenant_id} # Critical security filter
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
formatted_results = []
|
| 86 |
+
if results["documents"]:
|
| 87 |
+
for i, doc in enumerate(results["documents"][0]):
|
| 88 |
+
meta = results["metadatas"][0][i]
|
| 89 |
+
formatted_results.append({
|
| 90 |
+
"content": doc,
|
| 91 |
+
"metadata": meta,
|
| 92 |
+
"score": results["distances"][0][i] if results["distances"] else None
|
| 93 |
+
})
|
| 94 |
+
|
| 95 |
+
duration = (time.time() - start_time) * 1000
|
| 96 |
+
log_trace("mcp-rag-secure", trace_id, span_id, "query_knowledge_base", duration, "ok")
|
| 97 |
+
return formatted_results
|
| 98 |
+
except Exception as e:
|
| 99 |
+
duration = (time.time() - start_time) * 1000
|
| 100 |
+
log_trace("mcp-rag-secure", trace_id, span_id, "query_knowledge_base", duration, "error")
|
| 101 |
+
raise e
|
| 102 |
|
| 103 |
@mcp.tool()
|
| 104 |
def delete_tenant_data(tenant_id: str) -> str:
|
src/mcp-seo/server.py
CHANGED
|
@@ -9,7 +9,9 @@ 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")
|
|
@@ -20,7 +22,11 @@ def analyze_seo(url: str) -> Dict[str, Any]:
|
|
| 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')
|
|
@@ -57,9 +63,15 @@ def analyze_seo(url: str) -> Dict[str, Any]:
|
|
| 57 |
else:
|
| 58 |
result["external_links"] += 1
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
return result
|
| 61 |
except Exception as e:
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
|
| 64 |
@mcp.tool()
|
| 65 |
def analyze_ada(url: str) -> Dict[str, Any]:
|
|
|
|
| 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, log_trace, log_metric
|
| 13 |
+
import uuid
|
| 14 |
+
import time
|
| 15 |
|
| 16 |
# Initialize FastMCP Server
|
| 17 |
mcp = FastMCP("SEO & ADA Audit", host="0.0.0.0")
|
|
|
|
| 22 |
Perform a basic SEO audit of a webpage.
|
| 23 |
Checks title, meta description, H1 tags, image alt attributes, and internal/external links.
|
| 24 |
"""
|
| 25 |
+
start_time = time.time()
|
| 26 |
+
trace_id = str(uuid.uuid4())
|
| 27 |
+
span_id = str(uuid.uuid4())
|
| 28 |
log_usage("mcp-seo", "analyze_seo")
|
| 29 |
+
|
| 30 |
try:
|
| 31 |
response = requests.get(url, timeout=10)
|
| 32 |
soup = BeautifulSoup(response.content, 'html.parser')
|
|
|
|
| 63 |
else:
|
| 64 |
result["external_links"] += 1
|
| 65 |
|
| 66 |
+
duration = (time.time() - start_time) * 1000
|
| 67 |
+
log_trace("mcp-seo", trace_id, span_id, "analyze_seo", duration, "ok")
|
| 68 |
+
log_metric("mcp-seo", "seo_links_found", result["internal_links"] + result["external_links"], {"url": url})
|
| 69 |
+
|
| 70 |
return result
|
| 71 |
except Exception as e:
|
| 72 |
+
duration = (time.time() - start_time) * 1000
|
| 73 |
+
log_trace("mcp-seo", trace_id, span_id, "analyze_seo", duration, "error")
|
| 74 |
+
return [{"error": str(e)}]
|
| 75 |
|
| 76 |
@mcp.tool()
|
| 77 |
def analyze_ada(url: str) -> Dict[str, Any]:
|
src/mcp-trader/server.py
CHANGED
|
@@ -13,7 +13,9 @@ if src_dir not in sys.path:
|
|
| 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:
|
|
@@ -45,8 +47,25 @@ mcp = FastMCP("MCP Trader", host="0.0.0.0")
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
@mcp.tool()
|
| 52 |
def get_stock_fundamentals(symbol: str) -> Dict[str, Any]:
|
|
|
|
| 13 |
|
| 14 |
from mcp.server.fastmcp import FastMCP
|
| 15 |
from typing import List, Dict, Any
|
| 16 |
+
from core.mcp_telemetry import log_usage, log_trace, log_metric
|
| 17 |
+
import uuid
|
| 18 |
+
import time
|
| 19 |
|
| 20 |
# Local imports (assuming src/mcp-trader is a package or run from src)
|
| 21 |
try:
|
|
|
|
| 47 |
@mcp.tool()
|
| 48 |
def get_stock_price(symbol: str) -> float:
|
| 49 |
"""Get the current price for a stock symbol."""
|
| 50 |
+
start_time = time.time()
|
| 51 |
+
trace_id = str(uuid.uuid4())
|
| 52 |
+
span_id = str(uuid.uuid4())
|
| 53 |
+
|
| 54 |
log_usage("mcp-trader", "get_stock_price")
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
price = get_current_price(symbol)
|
| 58 |
+
duration = (time.time() - start_time) * 1000
|
| 59 |
+
|
| 60 |
+
# Metric
|
| 61 |
+
log_metric("mcp-trader", "stock_price", price, {"symbol": symbol})
|
| 62 |
+
log_trace("mcp-trader", trace_id, span_id, "get_stock_price", duration, "ok")
|
| 63 |
+
|
| 64 |
+
return price
|
| 65 |
+
except Exception as e:
|
| 66 |
+
duration = (time.time() - start_time) * 1000
|
| 67 |
+
log_trace("mcp-trader", trace_id, span_id, "get_stock_price", duration, "error")
|
| 68 |
+
raise e
|
| 69 |
|
| 70 |
@mcp.tool()
|
| 71 |
def get_stock_fundamentals(symbol: str) -> Dict[str, Any]:
|
src/mcp-trading-research/server.py
CHANGED
|
@@ -8,7 +8,9 @@ import yfinance as yf
|
|
| 8 |
from textblob import TextBlob
|
| 9 |
from mcp.server.fastmcp import FastMCP
|
| 10 |
from typing import List, Dict, Any
|
| 11 |
-
from core.mcp_telemetry import log_usage
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# Initialize FastMCP Server
|
| 14 |
mcp = FastMCP("Trading Research", host="0.0.0.0")
|
|
@@ -18,7 +20,11 @@ def get_news_sentiment(symbol: str) -> List[Dict[str, Any]]:
|
|
| 18 |
"""
|
| 19 |
Get recent news and analyze sentiment for a stock symbol.
|
| 20 |
"""
|
|
|
|
|
|
|
|
|
|
| 21 |
log_usage("mcp-trading-research", "get_news_sentiment")
|
|
|
|
| 22 |
try:
|
| 23 |
ticker = yf.Ticker(symbol)
|
| 24 |
news = ticker.news
|
|
@@ -46,8 +52,13 @@ def get_news_sentiment(symbol: str) -> List[Dict[str, Any]]:
|
|
| 46 |
"sentiment_score": round(sentiment, 2),
|
| 47 |
"sentiment_label": sentiment_label
|
| 48 |
})
|
|
|
|
|
|
|
|
|
|
| 49 |
return results
|
| 50 |
except Exception as e:
|
|
|
|
|
|
|
| 51 |
return [{"error": str(e)}]
|
| 52 |
|
| 53 |
@mcp.tool()
|
|
|
|
| 8 |
from textblob import TextBlob
|
| 9 |
from mcp.server.fastmcp import FastMCP
|
| 10 |
from typing import List, Dict, Any
|
| 11 |
+
from core.mcp_telemetry import log_usage, log_trace, log_metric
|
| 12 |
+
import uuid
|
| 13 |
+
import time
|
| 14 |
|
| 15 |
# Initialize FastMCP Server
|
| 16 |
mcp = FastMCP("Trading Research", host="0.0.0.0")
|
|
|
|
| 20 |
"""
|
| 21 |
Get recent news and analyze sentiment for a stock symbol.
|
| 22 |
"""
|
| 23 |
+
start_time = time.time()
|
| 24 |
+
trace_id = str(uuid.uuid4())
|
| 25 |
+
span_id = str(uuid.uuid4())
|
| 26 |
log_usage("mcp-trading-research", "get_news_sentiment")
|
| 27 |
+
|
| 28 |
try:
|
| 29 |
ticker = yf.Ticker(symbol)
|
| 30 |
news = ticker.news
|
|
|
|
| 52 |
"sentiment_score": round(sentiment, 2),
|
| 53 |
"sentiment_label": sentiment_label
|
| 54 |
})
|
| 55 |
+
|
| 56 |
+
duration = (time.time() - start_time) * 1000
|
| 57 |
+
log_trace("mcp-trading-research", trace_id, span_id, "get_news_sentiment", duration, "ok")
|
| 58 |
return results
|
| 59 |
except Exception as e:
|
| 60 |
+
duration = (time.time() - start_time) * 1000
|
| 61 |
+
log_trace("mcp-trading-research", trace_id, span_id, "get_news_sentiment", duration, "error")
|
| 62 |
return [{"error": str(e)}]
|
| 63 |
|
| 64 |
@mcp.tool()
|
src/mcp-weather/server.py
CHANGED
|
@@ -11,7 +11,10 @@ if src_dir not in sys.path:
|
|
| 11 |
sys.path.append(src_dir)
|
| 12 |
|
| 13 |
from mcp.server.fastmcp import FastMCP
|
| 14 |
-
from core.mcp_telemetry import log_usage
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# Initialize FastMCP Server
|
| 17 |
mcp = FastMCP("Weather MCP", host="0.0.0.0")
|
|
@@ -30,25 +33,47 @@ def get_current_weather(location: str, units: str = "metric") -> Dict[str, Any]:
|
|
| 30 |
Get current weather for a specific city or location.
|
| 31 |
Units can be 'metric' (Celsius) or 'imperial' (Fahrenheit).
|
| 32 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
log_usage("mcp-weather", "get_current_weather")
|
| 34 |
if not OPENWEATHER_API_KEY:
|
|
|
|
| 35 |
return {"error": "OPENWEATHER_API_KEY not set"}
|
| 36 |
|
| 37 |
url = f"https://api.openweathermap.org/data/2.5/weather?q={location}&appid={OPENWEATHER_API_KEY}&units={units}"
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
"
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
@mcp.tool()
|
| 54 |
def get_forecast(location: str, units: str = "metric") -> List[Dict[str, Any]]:
|
|
|
|
| 11 |
sys.path.append(src_dir)
|
| 12 |
|
| 13 |
from mcp.server.fastmcp import FastMCP
|
| 14 |
+
from core.mcp_telemetry import log_usage, log_trace, log_metric
|
| 15 |
+
import uuid
|
| 16 |
+
import time
|
| 17 |
+
from datetime import datetime
|
| 18 |
|
| 19 |
# Initialize FastMCP Server
|
| 20 |
mcp = FastMCP("Weather MCP", host="0.0.0.0")
|
|
|
|
| 33 |
Get current weather for a specific city or location.
|
| 34 |
Units can be 'metric' (Celsius) or 'imperial' (Fahrenheit).
|
| 35 |
"""
|
| 36 |
+
start_time = time.time()
|
| 37 |
+
trace_id = str(uuid.uuid4())
|
| 38 |
+
span_id = str(uuid.uuid4())
|
| 39 |
+
|
| 40 |
log_usage("mcp-weather", "get_current_weather")
|
| 41 |
if not OPENWEATHER_API_KEY:
|
| 42 |
+
log_trace("mcp-weather", trace_id, span_id, "get_current_weather", 0, "error")
|
| 43 |
return {"error": "OPENWEATHER_API_KEY not set"}
|
| 44 |
|
| 45 |
url = f"https://api.openweathermap.org/data/2.5/weather?q={location}&appid={OPENWEATHER_API_KEY}&units={units}"
|
| 46 |
+
try:
|
| 47 |
+
response = requests.get(url, timeout=10)
|
| 48 |
+
duration = (time.time() - start_time) * 1000
|
| 49 |
+
|
| 50 |
+
if response.status_code != 200:
|
| 51 |
+
log_trace("mcp-weather", trace_id, span_id, "get_current_weather", duration, "error")
|
| 52 |
+
return {"error": f"Failed to fetch weather: {response.text}"}
|
| 53 |
+
|
| 54 |
+
data = response.json()
|
| 55 |
+
temp = data["main"]["temp"]
|
| 56 |
+
|
| 57 |
+
# Log Metrics
|
| 58 |
+
log_metric("mcp-weather", "weather_temperature", temp, {"location": location, "unit": units})
|
| 59 |
+
log_metric("mcp-weather", "api_latency", duration, {"endpoint": "weather"})
|
| 60 |
+
|
| 61 |
+
# Log Trace
|
| 62 |
+
log_trace("mcp-weather", trace_id, span_id, "get_current_weather", duration, "ok")
|
| 63 |
+
|
| 64 |
+
return {
|
| 65 |
+
"location": data.get("name"),
|
| 66 |
+
"condition": data["weather"][0]["description"],
|
| 67 |
+
"temperature": format_temp(temp, units),
|
| 68 |
+
"feels_like": format_temp(data["main"]["feels_like"], units),
|
| 69 |
+
"humidity": f"{data['main']['humidity']}%",
|
| 70 |
+
"wind_speed": f"{data['wind']['speed']} m/s",
|
| 71 |
+
"timestamp": data.get("dt")
|
| 72 |
+
}
|
| 73 |
+
except Exception as e:
|
| 74 |
+
duration = (time.time() - start_time) * 1000
|
| 75 |
+
log_trace("mcp-weather", trace_id, span_id, "get_current_weather", duration, "error")
|
| 76 |
+
return {"error": str(e)}
|
| 77 |
|
| 78 |
@mcp.tool()
|
| 79 |
def get_forecast(location: str, units: str = "metric") -> List[Dict[str, Any]]:
|
src/mcp-web/server.py
CHANGED
|
@@ -13,7 +13,9 @@ if src_dir not in sys.path:
|
|
| 13 |
|
| 14 |
from mcp.server.fastmcp import FastMCP
|
| 15 |
from typing import List, Dict, Any, Union
|
| 16 |
-
from core.mcp_telemetry import log_usage
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# Local imports
|
| 19 |
try:
|
|
@@ -48,8 +50,21 @@ def search(query: str, max_results: int = 5) -> List[Dict[str, Any]]:
|
|
| 48 |
Search the web for the given query using DuckDuckGo.
|
| 49 |
Returns a list of results with title, url, snippet.
|
| 50 |
"""
|
|
|
|
|
|
|
|
|
|
| 51 |
log_usage("mcp-web", "search")
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
@mcp.tool()
|
| 55 |
def extract(url: str) -> str:
|
|
|
|
| 13 |
|
| 14 |
from mcp.server.fastmcp import FastMCP
|
| 15 |
from typing import List, Dict, Any, Union
|
| 16 |
+
from core.mcp_telemetry import log_usage, log_trace, log_metric
|
| 17 |
+
import uuid
|
| 18 |
+
import time
|
| 19 |
|
| 20 |
# Local imports
|
| 21 |
try:
|
|
|
|
| 50 |
Search the web for the given query using DuckDuckGo.
|
| 51 |
Returns a list of results with title, url, snippet.
|
| 52 |
"""
|
| 53 |
+
start_time = time.time()
|
| 54 |
+
trace_id = str(uuid.uuid4())
|
| 55 |
+
span_id = str(uuid.uuid4())
|
| 56 |
log_usage("mcp-web", "search")
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
results = search_web(query, max_results)
|
| 60 |
+
duration = (time.time() - start_time) * 1000
|
| 61 |
+
log_trace("mcp-web", trace_id, span_id, "search", duration, "ok")
|
| 62 |
+
log_metric("mcp-web", "search_results_count", len(results), {"query": query})
|
| 63 |
+
return results
|
| 64 |
+
except Exception as e:
|
| 65 |
+
duration = (time.time() - start_time) * 1000
|
| 66 |
+
log_trace("mcp-web", trace_id, span_id, "search", duration, "error")
|
| 67 |
+
raise e
|
| 68 |
|
| 69 |
@mcp.tool()
|
| 70 |
def extract(url: str) -> str:
|