Spaces:
Running
Running
File size: 12,387 Bytes
85a0eea 9b296b2 85a0eea aec9b35 1e2a207 1bb47d3 c9d4ca5 1e2a207 5a09c18 aec9b35 1bb47d3 aec9b35 85a0eea d7a2bef 948a289 d7a2bef 948a289 d7a2bef 948a289 d7a2bef 85a0eea 9b296b2 85a0eea 9b296b2 85a0eea 9b296b2 | 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 | # =============================================================================
# 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')
# ββ NEU: nach den Imports, vor app = Quart(__name__) ββββββββββββββββββββββββββ
def _make_mount_middleware(outer_app, path_prefix: str, inner_app):
"""
Minimale ASGI-Middleware: leitet Requests mit path_prefix an inner_app
(FastMCP Streamable HTTP) weiter, alles andere geht an outer_app (Quart).
Nur aktiv bei HUB_TRANSPORT = "streamable-http".
"""
async def middleware(scope, receive, send):
path = scope.get("path", "")
if path == path_prefix or path.startswith(path_prefix + "/"):
scope = dict(scope)
stripped = path[len(path_prefix):] or "/"
scope["path"] = stripped
scope["raw_path"] = stripped.encode()
await inner_app(scope, receive, send)
else:
await outer_app(scope, receive, send)
return middleware
# =============================================================================
# Quart app instance
# =============================================================================
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()),
})
@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
# 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!
# PSQL bridge β nur wenn Guardian DB-Service injiziert hat
# app.py β bridge-Block:
await db_sync.initialize()
if db_service:
# asyncpg Pool direkt nutzen β kein execute_secured_query nΓΆtig
db_sync.set_psql_writer(db_service.execute)
logger.info("PostgreSQL bridge active.")
else:
logger.info("PostgreSQL bridge inactive β no DATABASE_URL configured.")
await mcp.initialize()
# --- Transport-abhΓ€ngiges MCP-Routing ---
hub_cfg = app_config.get_hub()
transport = hub_cfg.get("HUB_TRANSPORT", "streamable-http").lower()
if transport == "streamable-http":
# ASGI-Mount: FastMCP ΓΌbernimmt /mcp direkt β kein Quart-Overhead
app.asgi_app = _make_mount_middleware(app.asgi_app, "/mcp", mcp.get_asgi_app())
logger.info("MCP transport: Streamable HTTP β /mcp")
else:
# SSE legacy β Quart-Route dynamisch registrieren
@app.route("/mcp", methods=["GET", "POST"])
async def mcp_endpoint():
"""MCP SSE legacy transport β interceptor point fΓΌr auth/logging."""
return await mcp.handle_request(request)
logger.info("MCP transport: SSE (legacy) β /mcp")
# --- Read PORT from app/.pyfun [HUB] ---
port = int(hub_cfg.get("HUB_PORT", "7860")) # hub_cfg bereits gelesen, kein zweiter get_hub()-Call
# --- 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)) |