Backup automatico script del 2026-08-23 12:04

This commit is contained in:
daniele committed 2026-08-23 12:04:16 +02:00
1 parent 11823447a2
commit 795a15a7b6
66 files changed
+8546

No files matched your search

+368
View File
@@ -0,0 +1,368 @@
# -*- coding: utf-8 -*-
"""Loogle MCP Hub — gateway FastAPI + OAuth + MCP Streamable HTTP."""
import json
import logging
import os
import secrets
from typing import Optional
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 . import audit, auth, jwt_utils, oauth
from .db import get_conn, init_db
from .mcp import server as mcp_server
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
LOGGER = logging.getLogger("loogle_mcp.main")
app = FastAPI(title="Loogle MCP Hub", docs_url=None, redoc_url=None)
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://claude.ai",
"https://chatgpt.com",
"https://chat.openai.com",
],
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
)
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
@app.on_event("startup")
def on_startup() -> None:
init_db()
auth.ensure_sessions_table()
auth.ensure_family_users()
oauth.ensure_default_client()
if not os.environ.get("MCP_JWT_SECRET", "").strip():
secret = secrets.token_urlsafe(48)
os.environ["MCP_JWT_SECRET"] = secret
LOGGER.warning("MCP_JWT_SECRET generato — salvalo in .env: %s", secret)
# ------------------------------------------------------------------ Health
@app.get("/health")
def health():
return {"ok": True, "service": "loogle-mcp", "port": int(os.environ.get("MCP_PORT", "8700"))}
# ------------------------------------------------------------------ OAuth metadata
@app.get("/.well-known/oauth-authorization-server")
def oauth_metadata():
return oauth.authorization_server_metadata()
@app.get("/.well-known/oauth-protected-resource")
def protected_resource():
return oauth.protected_resource_metadata()
@app.get("/.well-known/oauth-protected-resource/mcp")
def protected_resource_mcp():
return oauth.protected_resource_metadata()
@app.get("/.well-known/openid-configuration")
def openid_configuration():
"""Fallback discovery usato da Claude se oauth-authorization-server non basta."""
return oauth.authorization_server_metadata()
# ------------------------------------------------------------------ OAuth endpoints
class RegisterBody(BaseModel):
client_name: str
redirect_uris: list[str]
@app.post("/oauth/register")
def oauth_register(body: RegisterBody):
try:
return oauth.register_client(body.client_name, body.redirect_uris)
except HTTPException as exc:
return JSONResponse(
status_code=exc.status_code,
content={"error": "invalid_client_metadata", "error_description": str(exc.detail)},
)
# Alias root-level OAuth (Claude/ChatGPT fallback se la discovery RFC 8414 fallisce)
@app.post("/register")
def oauth_register_root(body: RegisterBody):
return oauth_register(body)
@app.get("/oauth/authorize")
def oauth_authorize_get(
response_type: str,
client_id: str,
redirect_uri: str,
scope: str = "context:read context:write knowledge:read knowledge:write gitea:read gitea:write",
state: str = "",
code_challenge: Optional[str] = None,
code_challenge_method: Optional[str] = None,
):
if response_type != "code":
raise HTTPException(400, "response_type must be code")
html = _login_form(client_id, redirect_uri, scope, state, code_challenge, code_challenge_method)
return HTMLResponse(html)
@app.post("/oauth/authorize")
def oauth_authorize_post(
request: Request,
client_id: str = Form(...),
redirect_uri: str = Form(...),
scope: str = Form("context:read context:write knowledge:read knowledge:write gitea:read gitea:write"),
state: str = Form(""),
code_challenge: Optional[str] = Form(None),
code_challenge_method: Optional[str] = Form(None),
username: str = Form(...),
password: str = Form(...),
):
ip = request.client.host if request.client else "?"
auth.throttle(ip)
user = auth.authenticate(username, password)
if not user:
auth.record_attempt(ip)
html = _login_form(
client_id, redirect_uri, scope, state, code_challenge, code_challenge_method,
error="Credenziali non valide",
)
return HTMLResponse(html, status_code=401)
url = oauth.build_authorize_redirect(
client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, user["id"]
)
return RedirectResponse(url, status_code=302)
@app.get("/authorize")
def oauth_authorize_get_root(
response_type: str,
client_id: str,
redirect_uri: str,
scope: str = "context:read context:write knowledge:read knowledge:write gitea:read gitea:write",
state: str = "",
code_challenge: Optional[str] = None,
code_challenge_method: Optional[str] = None,
):
return oauth_authorize_get(
response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method
)
@app.post("/authorize")
def oauth_authorize_post_root(
request: Request,
client_id: str = Form(...),
redirect_uri: str = Form(...),
scope: str = Form("context:read context:write knowledge:read knowledge:write gitea:read gitea:write"),
state: str = Form(""),
code_challenge: Optional[str] = Form(None),
code_challenge_method: Optional[str] = Form(None),
username: str = Form(...),
password: str = Form(...),
):
return oauth_authorize_post(
request, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, username, password
)
@app.post("/oauth/token")
async def oauth_token(request: Request):
content_type = request.headers.get("content-type", "")
if "application/json" in content_type:
body = await request.json()
else:
form = await request.form()
body = dict(form)
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(
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)
raise HTTPException(400, "grant_type non supportato")
@app.post("/token")
async def oauth_token_root(request: Request):
return await oauth_token(request)
# ------------------------------------------------------------------ MCP endpoint
@app.post("/mcp")
async def mcp_post(request: Request):
auth_header = request.headers.get("authorization", "")
claims = oauth.bearer_claims_from_header(auth_header)
try:
payload = await request.json()
except Exception:
raise HTTPException(400, "JSON non valido")
if isinstance(payload, list):
responses = mcp_server.handle_batch(payload, claims)
return JSONResponse(responses)
response = mcp_server.handle_message(payload, claims)
if not claims and payload.get("method") not in ("initialize", "notifications/initialized", "ping"):
return JSONResponse(response, status_code=401, headers=_auth_challenge_headers())
return JSONResponse(response)
@app.get("/mcp")
def mcp_get():
return JSONResponse(
{"error": "Use POST for MCP JSON-RPC"},
status_code=405,
headers=_auth_challenge_headers(),
)
def _auth_challenge_headers() -> dict:
base = jwt_utils.base_url()
resource_metadata = f"{base}/.well-known/oauth-protected-resource/mcp"
return {
"WWW-Authenticate": f'Bearer realm="mcp", resource_metadata="{resource_metadata}"',
}
# ------------------------------------------------------------------ Dashboard web
class LoginBody(BaseModel):
username: str
password: str
class PasswordBody(BaseModel):
old_password: str
new_password: str
class RevokeBody(BaseModel):
refresh_token: str
@app.post("/api/login")
def api_login(body: LoginBody, request: Request, response: Response):
ip = request.client.host if request.client else "?"
auth.throttle(ip)
user = auth.authenticate(body.username.strip(), body.password)
if not user:
auth.record_attempt(ip)
raise HTTPException(401, "Credenziali non valide")
import datetime
token = secrets.token_urlsafe(32)
expires = (
datetime.datetime.utcnow() + datetime.timedelta(days=auth.SESSION_DAYS)
).strftime("%Y-%m-%d %H:%M:%S")
get_conn().execute(
"INSERT INTO sessions(token,user_id,expires_at) VALUES (?,?,?)",
(token, user["id"], expires),
)
get_conn().commit()
response.set_cookie("mcp_session", token, max_age=auth.SESSION_DAYS * 86400, httponly=True, samesite="lax", path="/")
return {"ok": True, "user": {"username": user["username"], "is_admin": user["is_admin"]}}
@app.post("/api/logout")
def api_logout(request: Request, response: Response):
token = request.cookies.get("mcp_session", "")
if token:
get_conn().execute("DELETE FROM sessions WHERE token=?", (token,))
get_conn().commit()
response.delete_cookie("mcp_session", path="/")
return {"ok": True}
@app.get("/api/me")
def api_me(user=Depends(auth.current_user_from_cookie)):
return user
@app.post("/api/password")
def api_password(body: PasswordBody, user=Depends(auth.current_user_from_cookie)):
if len(body.new_password.strip()) < 6:
raise HTTPException(400, "La nuova password deve avere almeno 6 caratteri")
if not auth.change_password(user["id"], body.old_password, body.new_password):
raise HTTPException(400, "Password attuale errata")
return {"ok": True}
@app.get("/api/projects")
def api_projects(user=Depends(auth.current_user_from_cookie)):
from .context import store as context_store
return context_store.list_projects(user["username"])
@app.get("/api/audit")
def api_audit(limit: int = 100, user=Depends(auth.current_user_from_cookie)):
if user["is_admin"]:
return audit.list_audit(limit=min(limit, 500))
return audit.list_audit(limit=min(limit, 200), username=user["username"])
@app.post("/api/admin/revoke-refresh")
def api_revoke_refresh(body: RevokeBody, _=Depends(auth.require_admin)):
jwt_utils.revoke_refresh_token(body.refresh_token)
return {"ok": True}
@app.get("/dashboard")
def dashboard_page():
path = os.path.join(STATIC_DIR, "dashboard.html")
return HTMLResponse(open(path, encoding="utf-8").read())
@app.get("/")
def root():
return RedirectResponse("/dashboard")
def _login_form(
client_id: str,
redirect_uri: str,
scope: str,
state: str,
code_challenge: Optional[str],
code_challenge_method: Optional[str],
error: str = "",
) -> str:
err = f'<p class="error">{error}</p>' if error else ""
return f"""<!DOCTYPE html>
<html lang="it"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Loogle MCP — Login</title>
<style>
body{{font-family:system-ui,sans-serif;max-width:420px;margin:4rem auto;padding:1rem;background:#0f172a;color:#e2e8f0}}
h1{{font-size:1.4rem}} .card{{background:#1e293b;padding:1.5rem;border-radius:12px}}
label{{display:block;margin:.75rem 0 .25rem}} input{{width:100%;padding:.5rem;border-radius:6px;border:1px solid #334155;background:#0f172a;color:#e2e8f0}}
button{{margin-top:1rem;width:100%;padding:.65rem;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer}}
.error{{color:#f87171}} .hint{{font-size:.85rem;color:#94a3b8;margin-top:1rem}}
</style></head><body>
<h1>Loogle MCP Hub</h1>
<p>Accedi con le credenziali famiglia per collegare Claude, ChatGPT o Gemini.</p>
<div class="card">{err}
<form method="post" action="/authorize">
<input type="hidden" name="client_id" value="{client_id}">
<input type="hidden" name="redirect_uri" value="{redirect_uri}">
<input type="hidden" name="scope" value="{scope}">
<input type="hidden" name="state" value="{state}">
<input type="hidden" name="code_challenge" value="{code_challenge or ''}">
<input type="hidden" name="code_challenge_method" value="{code_challenge_method or ''}">
<label>Utente</label><input name="username" autocomplete="username" required>
<label>Password</label><input name="password" type="password" autocomplete="current-password" required>
<button type="submit">Autorizza accesso MCP</button>
</form>
<p class="hint">Primo accesso: password = username (es. lucia/lucia). Cambiala dal dashboard.</p>
</div></body></html>"""