Alibrown's picture
Update app/app.py
8c8802e verified
# =============================================================================
# root/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 RULES:
# - fundaments dict is ONLY unpacked inside start_application()
# - fundaments are NEVER stored globally or passed to other app/* modules
# - app/* modules read their own config from app/.pyfun
# - app/* internal state/IPC uses app/db_sync.py (SQLite) β€” NOT postgresql.py
# - Secrets stay in .env β†’ Guardian reads them β†’ never touched by app/*
# =============================================================================
from quart import Quart, request, jsonify # async Flask β€” ASGI compatible
import logging
from hypercorn.asyncio import serve # ASGI server β€” async native, replaces waitress
from hypercorn.config import Config # hypercorn config
import threading # for future tools that need own threads
import requests # sync HTTP for future tool workers
import time
from datetime import datetime
import asyncio
from typing import Dict, Any, Optional
# =============================================================================
# Import app/* modules β€” MINIMAL BUILD
# Each module reads its own config from app/.pyfun independently.
# NO fundaments passed into these modules!
# =============================================================================
from . import mcp # MCP transport layer (SSE via Quart route)
from . import config as app_config # app/.pyfun parser β€” used only in app/*
from . import providers # API provider registry β€” reads app/.pyfun
from . import models # Model config + token/rate limits β€” reads app/.pyfun
from . import tools # MCP tool definitions + provider mapping β€” reads app/.pyfun
from . import db_sync # Internal SQLite IPC β€” app/* state & communication
# # db_sync β‰  postgresql.py! Cloud DB is Guardian-only.
# Future modules (will uncomment 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_mcp = logging.getLogger('mcp')
logger_config = logging.getLogger('config')
logger_tools = logging.getLogger('tools')
logger_providers = logging.getLogger('providers')
logger_models = logging.getLogger('models')
logger_db_sync = logging.getLogger('db_sync')
# =============================================================================
# Quart app instance OLD
# =============================================================================
# app = Quart(__name__)
#START_TIME = datetime.utcnow()
# =============================================================================
# Quart 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()),
# })
# =============================================================================
# Quart app instance NEW
# =============================================================================
app = Quart(__name__)
START_TIME = datetime.utcnow()
_HEALTH_HTML = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="10">
<title>LLM-API-Gateway</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:ui-monospace,monospace;background:#0d1117;color:#c9d1d9;
min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem}
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;
padding:2rem 2.5rem;max-width:480px;width:100%}
.header{display:flex;align-items:center;gap:12px;margin-bottom:1.5rem;
padding-bottom:1.5rem;border-bottom:1px solid #21262d}
.dot{width:10px;height:10px;border-radius:50%;background:#3fb950;flex-shrink:0}
h1{font-size:1.1rem;font-weight:600;color:#f0f6fc}
.badge{margin-left:auto;font-size:.7rem;background:#1f6feb30;color:#58a6ff;
border:1px solid #1f6feb;border-radius:20px;padding:2px 10px}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:1.5rem}
.stat{background:#0d1117;border:1px solid #21262d;border-radius:8px;padding:.75rem 1rem}
.stat-label{font-size:.7rem;color:#8b949e;margin-bottom:4px}
.stat-value{font-size:1rem;color:#f0f6fc;font-weight:500}
.green{color:#3fb950}
.links{display:flex;flex-direction:column;gap:8px}
.link{display:flex;align-items:center;gap:10px;padding:.6rem .9rem;
background:#0d1117;border:1px solid #21262d;border-radius:8px;
color:#8b949e;text-decoration:none;font-size:.82rem;
transition:border-color .15s,color .15s}
.link:hover{border-color:#58a6ff;color:#58a6ff}
.arrow{font-size:.75rem;opacity:.4}
</style>
</head>
<body>
<div class="card">
<div class="header">
<div class="dot"></div>
<h1>LLM-API-Gateway</h1>
<span class="badge">running</span>
</div>
<div class="stats">
<div class="stat">
<div class="stat-label">status</div>
<div class="stat-value green">operational</div>
</div>
<div class="stat">
<div class="stat-label">uptime</div>
<div class="stat-value">__UPTIME__</div>
</div>
</div>
<div class="links">
<a class="link" href="https://huggingface.co/spaces/codey-lab/Multi-LLM-API-Gateway" target="_blank">
<span>HuggingFace Space</span><span class="arrow">β†—</span>
</a>
<a class="link" href="https://github.com/VolkanSah/Multi-LLM-API-Gateway" target="_blank">
<span>GitHub Repository</span><span class="arrow">β†—</span>
</a>
</div>
</div>
</body>
</html>"""
# =============================================================================
# Quart 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
uptime_s = int(uptime.total_seconds())
if "text/html" not in request.headers.get("Accept", ""):
return jsonify({
"status": "running",
"service": "Universal MCP Hub",
"uptime_seconds": uptime_s,
})
# Uptime formatieren
h = uptime_s // 3600
m = (uptime_s % 3600) // 60
s = uptime_s % 60
if h > 0:
uptime_str = f"{h}h {m}m {s}s"
elif m > 0:
uptime_str = f"{m}m {s}s"
else:
uptime_str = f"{s}s"
html = _HEALTH_HTML.replace("__UPTIME__", uptime_str)
return html, 200, {"Content-Type": "text/html"}
# end health_check
@app.route("/api", methods=["POST"])
async def api_endpoint():
try:
data = await request.get_json()
tool_name = data.get("tool")
params = data.get("params", {})
# System tools β€” handle directly, no prompt needed!
if tool_name == "list_active_tools":
return jsonify({"result": {
"active_tools": tools.list_all(),
"active_llm_providers": providers.list_active_llm(),
"active_search_providers": providers.list_active_search(),
"available_models": models.list_all(),
}})
if tool_name == "health_check":
return jsonify({"result": {"status": "ok"}})
# db_query β€” handled by db_sync directly, not tools.run()
if tool_name == "db_query":
sql = params.get("sql", "")
result = await db_sync.query(sql)
return jsonify({"result": result})
# rename 'provider' β†’ 'provider_name' for tools.run()
if "provider" in params:
params["provider_name"] = params.pop("provider")
result = await tools.run(tool_name, **params)
return jsonify({"result": result})
except Exception as e:
logger.error(f"API error: {e}")
return jsonify({"error": str(e)}), 500
@app.route("/crypto", methods=["POST"])
async def crypto_endpoint():
"""
Encrypted API endpoint.
Encryption handled by app/* layer β€” no direct fundaments access here.
"""
# TODO: implement via app/* encryption wrapper
data = await request.get_json()
return jsonify({"status": "not_implemented"}), 501
@app.route("/mcp", methods=["GET", "POST"])
async def mcp_endpoint():
"""
MCP SSE Transport endpoint β€” routed through Quart/hypercorn.
All MCP traffic passes through here β€” enables interception, logging,
auth checks, rate limiting, payload transformation before reaching MCP.
"""
return await mcp.handle_request(request)
# 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 exclusively 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).
Services are unpacked here and NEVER stored globally or
passed into other app/* modules.
"""
logger.info("Application starting...")
# =========================================================================
# Unpack fundaments β€” ONLY here, NEVER elsewhere in app/*
# These are the 6 fundament services from fundaments/*
# =========================================================================
config_service = fundaments["config"] # fundaments/config_handler.py
db_service = fundaments["db"] # fundaments/postgresql.py β€” None if not configured
encryption_service = fundaments["encryption"] # fundaments/encryption.py β€” None if keys not set
access_control_service = fundaments["access_control"] # fundaments/access_control.py β€” None if no DB
user_handler_service = fundaments["user_handler"] # fundaments/user_handler.py β€” None if no DB
security_service = fundaments["security"] # fundaments/security.py β€” None if deps missing
# --- 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).")
# =========================================================================
# Initialize app/* internal services β€” MINIMAL BUILD
# Uncomment each line when the module is ready!
# =========================================================================
# await db_sync.initialize() # SQLite IPC store for app/* β€” unrelated to postgresql.py
# await providers.initialize() # reads app/.pyfun [LLM_PROVIDERS] [SEARCH_PROVIDERS] # in mcp_init
# await models.initialize() # reads app/.pyfun [MODELS] # in mcp_init
# await tools.initialize() # reads app/.pyfun [TOOLS]
# --- Initialize MCP (registers tools, prepares SSE handler) ---
# db_sync only if cloud_DB used to!
await db_sync.initialize()
await mcp.initialize()
# --- Read PORT from app/.pyfun [HUB] ---
port = int(app_config.get_hub().get("HUB_PORT", "7860"))
# --- Configure hypercorn ---
config = Config()
config.bind = [f"0.0.0.0:{port}"]
logger.info(f"Starting hypercorn on port {port}...")
logger.info("All services running.")
# --- Run hypercorn β€” blocks until shutdown ---
await serve(app, config)
# =============================================================================
# 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))