Backup automatico script del 2026-09-29 07:00
This commit is contained in:
1 parent
09dcbbdadb
commit
a8ddcdd255
2 files changed
+83
-9
No files matched your search
@@ -11,7 +11,7 @@ from typing import Optional
|
|||||||
import anyio
|
import anyio
|
||||||
from fastapi import Depends, FastAPI, Form, HTTPException, Request, Response
|
from fastapi import Depends, FastAPI, Form, HTTPException, Request, Response
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ app.add_middleware(
|
|||||||
"https://chatgpt.com",
|
"https://chatgpt.com",
|
||||||
"https://chat.openai.com",
|
"https://chat.openai.com",
|
||||||
],
|
],
|
||||||
allow_methods=["GET", "POST", "OPTIONS"],
|
allow_methods=["GET", "POST", "HEAD", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
|
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
|
||||||
@@ -226,8 +226,10 @@ _UNAUTHENTICATED_METHODS = ("initialize", "notifications/initialized", "ping")
|
|||||||
async def mcp_post(request: Request):
|
async def mcp_post(request: Request):
|
||||||
header_version = request.headers.get("mcp-protocol-version")
|
header_version = request.headers.get("mcp-protocol-version")
|
||||||
if header_version and header_version not in mcp_server.SUPPORTED_PROTOCOL_VERSIONS:
|
if header_version and header_version not in mcp_server.SUPPORTED_PROTOCOL_VERSIONS:
|
||||||
return JSONResponse(
|
# Non bloccare: Claude/Anthropic mandano versioni nuove; negoziamo nel body initialize
|
||||||
{"error": f"MCP-Protocol-Version non supportata: {header_version}"}, status_code=400
|
LOGGER.warning(
|
||||||
|
"MCP-Protocol-Version header=%s non in lista locale — continuo (negoziazione JSON-RPC)",
|
||||||
|
header_version,
|
||||||
)
|
)
|
||||||
auth_header = request.headers.get("authorization", "")
|
auth_header = request.headers.get("authorization", "")
|
||||||
claims = await run_in_threadpool(oauth.bearer_claims_from_header, auth_header)
|
claims = await run_in_threadpool(oauth.bearer_claims_from_header, auth_header)
|
||||||
@@ -285,15 +287,84 @@ def _timeout_error(msg: dict) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/mcp")
|
@app.post("/")
|
||||||
def mcp_get():
|
async def root_mcp_post(request: Request):
|
||||||
|
"""Alcuni client (Claude) dopo OAuth POST-ano sulla root invece che su /mcp."""
|
||||||
|
ctype = (request.headers.get("content-type") or "").lower()
|
||||||
|
accept = (request.headers.get("accept") or "").lower()
|
||||||
|
if "json" not in ctype and "json" not in accept and "event-stream" not in accept:
|
||||||
|
raise HTTPException(404, "Not Found")
|
||||||
|
return await mcp_post(request)
|
||||||
|
|
||||||
|
|
||||||
|
@app.api_route("/mcp", methods=["GET", "HEAD"])
|
||||||
|
async def mcp_get(request: Request):
|
||||||
|
"""Streamable HTTP: GET può aprire SSE; HEAD serve alla discovery di Claude."""
|
||||||
|
if request.method == "HEAD":
|
||||||
|
return Response(
|
||||||
|
status_code=200,
|
||||||
|
headers={
|
||||||
|
**_auth_challenge_headers(),
|
||||||
|
"Allow": "GET, HEAD, POST, OPTIONS",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
accept = (request.headers.get("accept") or "").lower()
|
||||||
|
if "text/event-stream" in accept:
|
||||||
|
# Nessun messaggio server-initiated: stream SSE vuoto che si chiude subito
|
||||||
|
async def _empty():
|
||||||
|
yield ": loogle-mcp ready\n\n"
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
_empty(),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers={
|
||||||
|
**_auth_challenge_headers(),
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
},
|
||||||
|
)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"error": "Use POST for MCP JSON-RPC"},
|
{
|
||||||
status_code=405,
|
"ok": True,
|
||||||
|
"service": "loogle-mcp",
|
||||||
|
"transport": "streamable-http",
|
||||||
|
"endpoint": "/mcp",
|
||||||
|
"hint": "Usa POST JSON-RPC (initialize, tools/list, tools/call)",
|
||||||
|
},
|
||||||
headers=_auth_challenge_headers(),
|
headers=_auth_challenge_headers(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.api_route("/mcp/", methods=["GET", "HEAD", "POST", "OPTIONS"])
|
||||||
|
async def mcp_slash(request: Request):
|
||||||
|
"""Evita redirect HTTP su /mcp/ (NPM) che rompe Claude."""
|
||||||
|
if request.method == "POST":
|
||||||
|
return await mcp_post(request)
|
||||||
|
if request.method == "OPTIONS":
|
||||||
|
return Response(
|
||||||
|
status_code=204,
|
||||||
|
headers={
|
||||||
|
"Allow": "GET, HEAD, POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Authorization, Content-Type, Accept, MCP-Protocol-Version, Mcp-Session-Id",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return await mcp_get(request)
|
||||||
|
|
||||||
|
|
||||||
|
@app.options("/mcp")
|
||||||
|
async def mcp_options():
|
||||||
|
return Response(
|
||||||
|
status_code=204,
|
||||||
|
headers={
|
||||||
|
"Allow": "GET, HEAD, POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Methods": "GET, HEAD, POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Authorization, Content-Type, Accept, MCP-Protocol-Version, Mcp-Session-Id",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _auth_challenge_headers() -> dict:
|
def _auth_challenge_headers() -> dict:
|
||||||
base = jwt_utils.base_url()
|
base = jwt_utils.base_url()
|
||||||
resource_metadata = f"{base}/.well-known/oauth-protected-resource/mcp"
|
resource_metadata = f"{base}/.well-known/oauth-protected-resource/mcp"
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ from . import tools
|
|||||||
LOGGER = logging.getLogger("loogle_mcp.server")
|
LOGGER = logging.getLogger("loogle_mcp.server")
|
||||||
# Versioni supportate, dalla più recente: si risponde con quella chiesta dal client
|
# Versioni supportate, dalla più recente: si risponde con quella chiesta dal client
|
||||||
# se supportata, altrimenti con la più recente (negoziazione MCP lifecycle)
|
# se supportata, altrimenti con la più recente (negoziazione MCP lifecycle)
|
||||||
SUPPORTED_PROTOCOL_VERSIONS = ("2025-06-18", "2025-03-26", "2024-11-05")
|
SUPPORTED_PROTOCOL_VERSIONS = ("2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05")
|
||||||
|
|
||||||
|
|
||||||
def negotiate_protocol_version(requested: Optional[str]) -> str:
|
def negotiate_protocol_version(requested: Optional[str]) -> str:
|
||||||
if requested in SUPPORTED_PROTOCOL_VERSIONS:
|
if requested in SUPPORTED_PROTOCOL_VERSIONS:
|
||||||
return requested
|
return requested
|
||||||
|
# Client più nuovi: accettiamo la richiesta dichiarando la più recente che implementiamo
|
||||||
|
if requested:
|
||||||
|
LOGGER.info("protocollo sconosciuto %s — negozio %s", requested, SUPPORTED_PROTOCOL_VERSIONS[0])
|
||||||
return SUPPORTED_PROTOCOL_VERSIONS[0]
|
return SUPPORTED_PROTOCOL_VERSIONS[0]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||