"""Entry point: runs both Telegram bot and MCP server in one process.""" import asyncio import logging import signal from starlette.middleware import Middleware from starlette.requests import Request from starlette.responses import JSONResponse, Response from .config import MCP_HOST, MCP_PORT, MEDIA_DIR, DB_PATH from .db import Database from .telegram_bot import BridgeBot from .mcp_server import mcp, init as init_mcp, custom_routes from .auth import auth_routes, validate_bearer_token logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) logger = logging.getLogger("mcp_bridge") # Paths that don't require auth PUBLIC_PATHS = { "/.well-known/oauth-authorization-server", "/token", "/api/health", "/api/ingest", # Local-only, not exposed via NPM } class AuthMiddleware: """ASGI middleware that validates Bearer tokens on protected endpoints.""" def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): if scope["type"] != "http": return await self.app(scope, receive, send) path = scope.get("path", "") # Skip auth for public paths if path in PUBLIC_PATHS: return await self.app(scope, receive, send) # Check Authorization header headers = dict(scope.get("headers", [])) auth_header = headers.get(b"authorization", b"").decode() if auth_header.startswith("Bearer "): token = auth_header[7:] if validate_bearer_token(token): return await self.app(scope, receive, send) # Reject — send 401 response = JSONResponse( {"error": "unauthorized", "error_description": "Valid Bearer token required"}, status_code=401, headers={"WWW-Authenticate": 'Bearer realm="mcp"'}, ) await response(scope, receive, send) async def run_telegram_bot(bot: BridgeBot): """Run the telegram bot polling loop.""" app = bot.build_application() await app.initialize() await bot._post_init(app) await app.start() updater = app.updater await updater.start_polling(drop_pending_updates=True) logger.info("Telegram bot polling started") try: while True: await asyncio.sleep(3600) except asyncio.CancelledError: logger.info("Telegram bot shutting down...") await updater.stop() await app.stop() await app.shutdown() async def run_mcp_server(): """Run the FastMCP HTTP server with OAuth auth and custom routes.""" import uvicorn # Get the FastMCP Starlette app mcp_app = mcp.http_app() # Add custom routes (API + OAuth) mcp_app.routes.extend(custom_routes) mcp_app.routes.extend(auth_routes) # Wrap with auth middleware authed_app = AuthMiddleware(mcp_app) logger.info(f"MCP server starting on {MCP_HOST}:{MCP_PORT} (OAuth enabled)") config = uvicorn.Config(authed_app, host=MCP_HOST, port=MCP_PORT, log_level="info") server = uvicorn.Server(config) await server.serve() async def main(): """Start both services.""" MEDIA_DIR.mkdir(parents=True, exist_ok=True) DB_PATH.parent.mkdir(parents=True, exist_ok=True) db = Database() logger.info(f"Database initialized at {DB_PATH}") init_mcp(db) bot = BridgeBot(db) telegram_task = asyncio.create_task(run_telegram_bot(bot)) mcp_task = asyncio.create_task(run_mcp_server()) loop = asyncio.get_event_loop() def handle_signal(): logger.info("Received shutdown signal") telegram_task.cancel() mcp_task.cancel() for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, handle_signal) try: await asyncio.gather(telegram_task, mcp_task, return_exceptions=True) except asyncio.CancelledError: pass logger.info("MCP Bridge stopped") if __name__ == "__main__": asyncio.run(main())