Spaces:
Sleeping
Sleeping
File size: 11,194 Bytes
3060aa0 6511c4e 3060aa0 6511c4e 3060aa0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | # =============================================================================
# app/app.py
# Universal MCP Hub (Sandboxed) - based on PyFundaments Architecture
# Copyright 2026 - Volkan Kücükbudak
# Apache License V. 2 + ESOL 1.1
# Repo: https://github.com/VolkanSah/Universal-MCP-Hub-sandboxed
# =============================================================================
# ARCHITECTURE NOTE:
# This file is the Orchestrator of the sandboxed app/* layer.
# It is ONLY started by main.py (the "Guardian").
# All fundament services are injected via the `fundaments` dictionary.
# Direct execution is blocked by design.
#
# SANDBOX RULE:
# app/* has NO direct access to .env or fundaments/*.
# Config for app/* lives in app/.pyfun (provider URLs, models, tool settings).
# Secrets stay in .env → Guardian reads them → injects what app/* needs.
# =============================================================================
from quart import Quart, request, jsonify # async Flask — required for async cloud providers + Neon DB
import logging
from waitress import serve # WSGI server — keeps Flask non-blocking alongside asyncio
import threading # bank-pattern: each blocking service gets its own thread
import requests # sync HTTP for health check worker
import time
from datetime import datetime
import asyncio
import sys
from typing import Dict, Any, Optional
# =============================================================================
# Import app/* modules
# Config/settings for all modules below live in app/.pyfun — not in .env!
# =============================================================================
#from . import mcp # MCP transport layer (stdio / SSE)
#from . import providers # API provider registry (LLM, Search, Web)
#from . import models # Model config + token/rate limits
#from . import tools # MCP tool definitions + provider mapping
#from . import db_sync # Internal SQLite IPC — app/* state & communication
# db_sync ≠ cloud DB! Cloud DB is Guardian-only via main.py.
# Future modules (soon uncommented when ready):
# from . import discord_api # Discord bot integration
# from . import hf_hooks # HuggingFace Space hooks
# from . import git_hooks # GitHub/GitLab webhook handler
# from . import web_api # Generic REST API handler
# =============================================================================
# Loggers — one per module for clean log filtering
# =============================================================================
logger = logging.getLogger('application')
#logger = logging.getLogger('config')
# logger_mcp = logging.getLogger('mcp')
# logger_tools = logging.getLogger('tools')
# logger_providers = logging.getLogger('providers')
# logger_models = logging.getLogger('models')
# logger_db_sync = logging.getLogger('db_sync')
# =============================================================================
# Flask app instance
# =============================================================================
app = Quart(__name__)
START_TIME = datetime.utcnow()
# =============================================================================
# Global service references (set during initialize_services)
# =============================================================================
_fundaments: Optional[Dict[str, Any]] = None
PORT = None
# =============================================================================
# Service initialization
# =============================================================================
def initialize_services(fundaments: Dict[str, Any]) -> None:
"""
Initializes all app/* services with injected fundaments from Guardian.
Called once during start_application — sets global service references.
"""
global _fundaments, PORT
_fundaments = fundaments
PORT = fundaments["config"].get_int("PORT", 7860)
# Initialize internal SQLite state store for app/* IPC
db_sync.initialize()
# Initialize provider registry from app/.pyfun + ENV key presence check
providers.initialize(fundaments["config"])
# Initialize model registry from app/.pyfun
models.initialize()
# Initialize tool registry — tools only register if their provider is active
tools.initialize(providers, models, fundaments)
logger.info("app/* services initialized.")
# =============================================================================
# Background workers
# =============================================================================
def start_mcp_in_thread() -> None:
"""
Starts the MCP Hub (stdio or SSE) in its own thread with its own event loop.
Mirrors the bank-thread pattern from the Discord bot architecture.
"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(mcp.start_mcp(_fundaments))
finally:
loop.close()
def health_check_worker() -> None:
"""
Periodic self-ping to keep the app alive on hosting platforms (e.g. HuggingFace).
Runs in its own daemon thread — does not block the main loop.
"""
while True:
time.sleep(3600)
try:
response = requests.get(f"http://127.0.0.1:{PORT}/")
logger.info(f"Health check ping: {response.status_code}")
except Exception as e:
logger.error(f"Health check failed: {e}")
# =============================================================================
# Flask Routes
# =============================================================================
@app.route("/", methods=["GET"])
async def health_check():
"""
Health check endpoint.
Used by HuggingFace Spaces and monitoring systems to verify the app is running.
"""
uptime = datetime.utcnow() - START_TIME
return jsonify({
"status": "running",
"service": "Universal MCP Hub",
"uptime_seconds": int(uptime.total_seconds()),
"active_providers": providers.get_active_names() if providers else [],
})
@app.route("/api", methods=["POST"])
async def api_endpoint():
"""
Generic REST API endpoint for direct tool invocation.
Accepts JSON: { "tool": "tool_name", "params": { ... } }
Auth and validation handled by tools layer.
"""
# TODO: implement tool dispatch via tools.invoke()
data = await request.get_json()
return jsonify({"status": "not_implemented", "received": data}), 501
@app.route("/crypto", methods=["POST"])
async def crypto_endpoint():
"""
Encrypted API endpoint.
Payload is decrypted via fundaments/encryption.py (injected by Guardian).
Only active if encryption_service is available in fundaments.
"""
encryption_service = _fundaments.get("encryption") if _fundaments else None
if not encryption_service:
return jsonify({"error": "Encryption service not available"}), 503
# TODO: decrypt payload, dispatch, re-encrypt response
data = await request.get_json()
return jsonify({"status": "not_implemented"}), 501
# Future routes (uncomment when ready):
# @app.route("/discord", methods=["POST"])
# async def discord_interactions():
# """Discord interactions endpoint — signature verification via discord_api module."""
# pass
# @app.route("/webhook/hf", methods=["POST"])
# async def hf_webhook():
# """HuggingFace Space event hooks."""
# pass
# @app.route("/webhook/git", methods=["POST"])
# async def git_webhook():
# """GitHub / GitLab webhook handler."""
# pass
# =============================================================================
# Main entry point — called by Guardian (main.py)
# =============================================================================
async def start_application(fundaments: Dict[str, Any]) -> None:
"""
Main entry point for the sandboxed app layer.
Called exclusively by main.py after all fundament services are initialized.
Args:
fundaments: Dictionary of initialized services from Guardian (main.py).
All services already validated — may be None if not configured.
"""
logger.info("Application starting...")
# --- Unpack fundament services (read-only references) ---
config_service = fundaments["config"]
db_service = fundaments["db"] # None if no DB configured
encryption_service = fundaments["encryption"] # None if keys not set
access_control_service = fundaments["access_control"] # None if no DB
user_handler_service = fundaments["user_handler"] # None if no DB
security_service = fundaments["security"] # None if deps missing
# --- Initialize all app/* services ---
initialize_services(fundaments)
# --- Log active fundament services ---
if encryption_service:
logger.info("Encryption service active.")
if user_handler_service and security_service:
logger.info("Auth services active (user_handler + security).")
if access_control_service and security_service:
logger.info("Access control active.")
if db_service and not user_handler_service:
logger.info("Database-only mode active (e.g. ML pipeline).")
if not db_service:
logger.info("Database-free mode active (e.g. Discord bot, API client).")
# --- Start MCP Hub in its own thread (stdio or SSE) ---
mcp_thread = threading.Thread(target=start_mcp_in_thread, daemon=True)
mcp_thread.start()
logger.info("MCP Hub thread started.")
# Allow MCP to initialize before Flask comes up
await asyncio.sleep(1)
# --- Start health check worker ---
health_thread = threading.Thread(target=health_check_worker, daemon=True)
health_thread.start()
# --- Start Flask/Quart via Waitress in its own thread ---
def run_server():
serve(app, host="0.0.0.0", port=PORT)
server_thread = threading.Thread(target=run_server, daemon=True)
server_thread.start()
logger.info(f"HTTP server started on port {PORT}.")
logger.info("All services running. Entering heartbeat loop...")
# --- Heartbeat loop — keeps Guardian's async context alive ---
try:
while True:
await asyncio.sleep(60)
logger.debug("Heartbeat.")
except KeyboardInterrupt:
logger.info("Shutdown signal received.")
# =============================================================================
# Direct execution guard
# =============================================================================
if __name__ == '__main__':
print("WARNING: Running app.py directly. Fundament modules might not be correctly initialized.")
print("Please run 'python main.py' instead for proper initialization.")
test_fundaments = {
"config": None,
"db": None,
"encryption": None,
"access_control": None,
"user_handler": None,
"security": None,
}
asyncio.run(start_application(test_fundaments))
|