fix: proxy-headers HTTPS redirect, httpx manquant, mcp<2.0.0, migration SSE->StreamableHTTP (30/07/2026)

This commit is contained in:
2026-07-30 21:41:19 +00:00
parent a29623d782
commit 41ea52ca2d
4 changed files with 34 additions and 27 deletions
+1 -1
View File
@@ -20,4 +20,4 @@ EXPOSE 8000
RUN mkdir -p /app/data RUN mkdir -p /app/data
# Run seed data on startup, then start uvicorn # Run seed data on startup, then start uvicorn
CMD ["sh", "-c", "python -m app.seed && uvicorn app.main:app --host 0.0.0.0 --port 8000"] CMD ["sh", "-c", "python -m app.seed && uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips=*"]
+8 -4
View File
@@ -1,11 +1,18 @@
import asyncio import asyncio
import contextlib
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.routes import api, admin, mcp from app.routes import api, admin, mcp
from app.models import init_db from app.models import init_db
app = FastAPI(title="context-hub", version="1.0.0") @contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
async with mcp.session_manager.run():
yield
app = FastAPI(title="context-hub", version="1.0.0", lifespan=lifespan)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@@ -21,9 +28,6 @@ app.include_router(api.router)
app.include_router(admin.router) app.include_router(admin.router)
app.include_router(mcp.router) app.include_router(mcp.router)
@app.on_event("startup")
async def on_startup():
await init_db()
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
+19 -17
View File
@@ -4,7 +4,10 @@ import contextvars
from fastapi import APIRouter, Depends, Request, HTTPException from fastapi import APIRouter, Depends, Request, HTTPException
from fastapi.responses import Response, JSONResponse from fastapi.responses import Response, JSONResponse
from mcp.server import Server from mcp.server import Server
from mcp.server.sse import SseServerTransport try:
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
except ImportError:
from mcp.server.streamable_http import StreamableHTTPSessionManager
from mcp.types import Tool, TextContent from mcp.types import Tool, TextContent
from app.auth import get_current_agent from app.auth import get_current_agent
from app.scopes import has_scope_access, ALL_SCOPES from app.scopes import has_scope_access, ALL_SCOPES
@@ -223,16 +226,20 @@ async def handle_call_tool(name: str, arguments: dict) -> list[TextContent]:
return [TextContent(type="text", text=json.dumps({"error": "Unknown tool"}))] return [TextContent(type="text", text=json.dumps({"error": "Unknown tool"}))]
# FastMCP / SSE Integration # Streamable HTTP Integration (mcp SDK v1.x, transport recommande depuis la spec 2025-03-26).
# connect_sse() ET handle_post_message() envoient chacun leur reponse ASGI # Remplace l ancien transport SSE (SseServerTransport) qui exigeait un session_id
# completement par eux-memes (via le send() qu'on leur passe). Les faire # etabli via un GET prealable -- les clients MCP modernes (Gemini/Antigravity inclus)
# passer par des routes FastAPI classiques (Depends + return Response) fait # postent directement le JSON-RPC initialize sans cette poignee de main, d ou les 400
# que FastAPI tente un second envoi une fois le SDK termine -> RuntimeError # Bad Request observes avec l ancienne implementation.
# uvicorn ("Unexpected ASGI message 'http.response.start' sent, after response # L auth X-API-Key reste verifiee a la main avant de deleguer a session_manager.handle_request,
# already completed"). Un seul montage ASGI brut gere GET (ouverture SSE) et # meme principe qu avant : current_agent_var est positionne pour la duree de la requete,
# POST (messages) sans jamais repasser par le wrapping FastAPI ; l'auth # lu ensuite par les handlers call_tool/list_tools via caller_agent.
# X-API-Key est donc verifiee ici a la main plutot que via Depends. session_manager = StreamableHTTPSessionManager(
sse_transport = SseServerTransport("/messages") app=mcp_server,
event_store=None,
json_response=True,
stateless=True,
)
async def _mcp_asgi_app(scope, receive, send): async def _mcp_asgi_app(scope, receive, send):
if scope["type"] != "http": if scope["type"] != "http":
@@ -247,14 +254,9 @@ async def _mcp_asgi_app(scope, receive, send):
return return
await update_key_last_used(api_key) await update_key_last_used(api_key)
if scope["method"] == "POST":
await sse_transport.handle_post_message(scope, receive, send)
return
async with sse_transport.connect_sse(scope, receive, send) as (read_stream, write_stream):
token = current_agent_var.set(agent) token = current_agent_var.set(agent)
try: try:
await mcp_server.run(read_stream, write_stream, mcp_server.create_initialization_options()) await session_manager.handle_request(scope, receive, send)
finally: finally:
current_agent_var.reset(token) current_agent_var.reset(token)
+2 -1
View File
@@ -4,6 +4,7 @@ aiosqlite>=0.19.0
PyJWT>=2.8.0 PyJWT>=2.8.0
jinja2>=3.1.3 jinja2>=3.1.3
python-multipart>=0.0.9 python-multipart>=0.0.9
mcp>=1.2.0 mcp>=1.2.0,<2.0.0
python-dotenv>=1.0.1 python-dotenv>=1.0.1
sse-starlette>=1.6.5 sse-starlette>=1.6.5
httpx>=0.27.0