Backup automatico script del 2026-09-27 07:00
This commit is contained in:
1 parent
f347badf3b
commit
09dcbbdadb
24 files changed
+1164
-193
No files matched your search
@@ -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(
|
||||
|
||||