mishrabp commited on
Commit
29574e5
·
verified ·
1 Parent(s): 7f7b279

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +1 -1
  2. src/core/__init__.py +0 -0
  3. src/core/mcp_telemetry.py +227 -0
  4. src/core/model.py +36 -0
  5. src/mcp-azure-sre/Dockerfile +22 -0
  6. src/mcp-azure-sre/README.md +31 -0
  7. src/mcp-azure-sre/server.py +167 -0
  8. src/mcp-github/Dockerfile +1 -1
  9. src/mcp-github/server.py +1 -1
  10. src/mcp-hub/.gitignore +24 -0
  11. src/mcp-hub/.vscode/extensions.json +3 -0
  12. src/mcp-hub/Dockerfile +40 -0
  13. src/mcp-hub/README.md +41 -0
  14. src/mcp-hub/api.py +383 -0
  15. src/mcp-hub/index.html +19 -0
  16. src/mcp-hub/nginx.conf +11 -0
  17. src/mcp-hub/package-lock.json +1341 -0
  18. src/mcp-hub/package.json +19 -0
  19. src/mcp-hub/public/logo-icon.svg +12 -0
  20. src/mcp-hub/public/logo.svg +12 -0
  21. src/mcp-hub/public/vite.svg +1 -0
  22. src/mcp-hub/src/App.vue +348 -0
  23. src/mcp-hub/src/assets/vue.svg +1 -0
  24. src/mcp-hub/src/components/HelloWorld.vue +43 -0
  25. src/mcp-hub/src/main.js +5 -0
  26. src/mcp-hub/src/style.css +680 -0
  27. src/mcp-hub/vite.config.js +7 -0
  28. src/mcp-rag-secure/Dockerfile +25 -0
  29. src/mcp-rag-secure/README.md +27 -0
  30. src/mcp-rag-secure/server.py +108 -0
  31. src/mcp-seo/Dockerfile +22 -0
  32. src/mcp-seo/README.md +23 -0
  33. src/mcp-seo/server.py +158 -0
  34. src/mcp-trader/Dockerfile +33 -0
  35. src/mcp-trader/README.md +24 -0
  36. src/mcp-trader/__init__.py +0 -0
  37. src/mcp-trader/config.py +13 -0
  38. src/mcp-trader/data/__init__.py +0 -0
  39. src/mcp-trader/data/fundamentals.py +24 -0
  40. src/mcp-trader/data/market_data.py +52 -0
  41. src/mcp-trader/indicators/__init__.py +0 -0
  42. src/mcp-trader/indicators/technical.py +43 -0
  43. src/mcp-trader/schemas.py +35 -0
  44. src/mcp-trader/server.py +135 -0
  45. src/mcp-trader/strategies/__init__.py +0 -0
  46. src/mcp-trader/strategies/bollinger_squeeze.py +71 -0
  47. src/mcp-trader/strategies/golden_cross.py +66 -0
  48. src/mcp-trader/strategies/macd_crossover.py +62 -0
  49. src/mcp-trader/strategies/mean_reversion.py +57 -0
  50. 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/mcp_telemetry.py ./src/mcp_telemetry.py
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/mcp_telemetry.py ./src/mcp_telemetry.py
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">&larr; 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
+ }