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
|
||||
from fastapi import Depends, FastAPI, Form, HTTPException, Request, Response
|
||||
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 starlette.concurrency import run_in_threadpool
|
||||
|
||||
@@ -30,7 +30,7 @@ app.add_middleware(
|
||||
"https://chatgpt.com",
|
||||
"https://chat.openai.com",
|
||||
],
|
||||
allow_methods=["GET", "POST", "OPTIONS"],
|
||||
allow_methods=["GET", "POST", "HEAD", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
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):
|
||||
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
|
||||
# Non bloccare: Claude/Anthropic mandano versioni nuove; negoziamo nel body initialize
|
||||
LOGGER.warning(
|
||||
"MCP-Protocol-Version header=%s non in lista locale — continuo (negoziazione JSON-RPC)",
|
||||
header_version,
|
||||
)
|
||||
auth_header = request.headers.get("authorization", "")
|
||||
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")
|
||||
def mcp_get():
|
||||
@app.post("/")
|
||||
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(
|
||||
{"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(),
|
||||
)
|
||||
|
||||
|
||||
@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:
|
||||
base = jwt_utils.base_url()
|
||||
resource_metadata = f"{base}/.well-known/oauth-protected-resource/mcp"
|
||||
|
||||
@@ -10,12 +10,15 @@ from . import tools
|
||||
LOGGER = logging.getLogger("loogle_mcp.server")
|
||||
# Versioni supportate, dalla più recente: si risponde con quella chiesta dal client
|
||||
# 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:
|
||||
if requested in SUPPORTED_PROTOCOL_VERSIONS:
|
||||
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]
|
||||
|
||||
|
||||
|
||||