Backup automatico script del 2026-09-27 07:00

This commit is contained in:
daniele committed 2026-09-27 07:00:02 +02:00
1 parent f347badf3b
commit 09dcbbdadb
24 files changed
+1164 -193

No files matched your search

+70 -6
View File
@@ -1,16 +1,19 @@
# -*- coding: utf-8 -*-
"""Loogle MCP Hub — gateway FastAPI + OAuth + MCP Streamable HTTP."""
import asyncio
import json
import logging
import os
import secrets
from typing import Optional
import anyio
from fastapi import Depends, FastAPI, Form, HTTPException, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from pydantic import BaseModel
from starlette.concurrency import run_in_threadpool
from . import audit, auth, jwt_utils, oauth
from .db import get_conn, init_db
@@ -31,6 +34,10 @@ app.add_middleware(
allow_headers=["*"],
)
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
# Thread per tool MCP ed endpoint sync: i tool fanno I/O bloccante (HTTP, SQLite, Ollama)
MCP_THREAD_LIMIT = int(os.environ.get("MCP_THREAD_LIMIT", "40"))
# Sotto il timeout dei client MCP: meglio un errore chiaro che una connessione appesa
MCP_TOOL_TIMEOUT_S = float(os.environ.get("MCP_TOOL_TIMEOUT_S", "45"))
@app.on_event("startup")
@@ -45,6 +52,11 @@ def on_startup() -> None:
LOGGER.warning("MCP_JWT_SECRET generato — salvalo in .env: %s", secret)
@app.on_event("startup")
async def configure_threadpool() -> None:
anyio.to_thread.current_default_thread_limiter().total_tokens = MCP_THREAD_LIMIT
# ------------------------------------------------------------------ Health
@app.get("/health")
@@ -186,14 +198,17 @@ async def oauth_token(request: Request):
grant_type = body.get("grant_type")
client_id = body.get("client_id") or os.environ.get("MCP_OAUTH_CLIENT_ID", "loogle-mcp-public")
if grant_type == "authorization_code":
return oauth.exchange_code(
return await run_in_threadpool(
oauth.exchange_code,
body.get("code", ""),
client_id,
body.get("redirect_uri", ""),
body.get("code_verifier"),
)
if grant_type == "refresh_token":
return oauth.refresh_access_token(body.get("refresh_token", ""), client_id)
return await run_in_threadpool(
oauth.refresh_access_token, body.get("refresh_token", ""), client_id
)
raise HTTPException(400, "grant_type non supportato")
@@ -204,23 +219,72 @@ async def oauth_token_root(request: Request):
# ------------------------------------------------------------------ MCP endpoint
_UNAUTHENTICATED_METHODS = ("initialize", "notifications/initialized", "ping")
@app.post("/mcp")
async def mcp_post(request: Request):
header_version = request.headers.get("mcp-protocol-version")
if header_version and header_version not in mcp_server.SUPPORTED_PROTOCOL_VERSIONS:
return JSONResponse(
{"error": f"MCP-Protocol-Version non supportata: {header_version}"}, status_code=400
)
auth_header = request.headers.get("authorization", "")
claims = oauth.bearer_claims_from_header(auth_header)
claims = await run_in_threadpool(oauth.bearer_claims_from_header, auth_header)
try:
payload = await request.json()
except Exception:
raise HTTPException(400, "JSON non valido")
messages = payload if isinstance(payload, list) else [payload]
if not all(isinstance(msg, dict) for msg in messages) or not messages:
return JSONResponse(
{"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Invalid Request"}},
status_code=400,
)
if not any(mcp_server.expects_response(msg) for msg in messages):
# Solo notifiche/response dal client: Streamable HTTP prevede 202 senza corpo
if not claims and any(msg.get("method") not in _UNAUTHENTICATED_METHODS for msg in messages):
return Response(status_code=401, headers=_auth_challenge_headers())
return Response(status_code=202)
if isinstance(payload, list):
responses = mcp_server.handle_batch(payload, claims)
responses = await _run_with_timeout(mcp_server.handle_batch, payload, claims)
if responses is None:
responses = [_timeout_error(msg) for msg in payload if mcp_server.expects_response(msg)]
return JSONResponse(responses)
response = mcp_server.handle_message(payload, claims)
if not claims and payload.get("method") not in ("initialize", "notifications/initialized", "ping"):
response = await _run_with_timeout(mcp_server.handle_message, payload, claims)
if response is None:
response = _timeout_error(payload)
if not claims and payload.get("method") not in _UNAUTHENTICATED_METHODS:
return JSONResponse(response, status_code=401, headers=_auth_challenge_headers())
return JSONResponse(response)
async def _run_with_timeout(fn, payload, claims):
"""I tool sono sincroni e bloccanti: threadpool per non fermare l'event loop.
Allo scadere ritorna None; il thread prosegue fino alla fine ma il client è già servito.
"""
try:
return await asyncio.wait_for(run_in_threadpool(fn, payload, claims), MCP_TOOL_TIMEOUT_S)
except asyncio.TimeoutError:
return None
def _timeout_error(msg: dict) -> dict:
params = msg.get("params") or {}
target = params.get("name") or msg.get("method")
LOGGER.warning("MCP %s oltre %.0fs: risposta di timeout al client", target, MCP_TOOL_TIMEOUT_S)
return {
"jsonrpc": "2.0",
"id": msg.get("id"),
"error": {
"code": -32000,
"message": f"Timeout: '{target}' non ha risposto entro {MCP_TOOL_TIMEOUT_S:.0f}s "
"(backend lento o non raggiungibile). Riprova tra poco.",
},
}
@app.get("/mcp")
def mcp_get():
return JSONResponse(
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.