diff --git a/Dockerfile b/Dockerfile index 12749a2..345e6ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,4 +20,4 @@ EXPOSE 8000 RUN mkdir -p /app/data # 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=*"] diff --git a/app/main.py b/app/main.py index 66838a9..2b84d60 100644 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,18 @@ import asyncio +import contextlib from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from app.routes import api, admin, mcp 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( CORSMiddleware, @@ -21,9 +28,6 @@ app.include_router(api.router) app.include_router(admin.router) app.include_router(mcp.router) -@app.on_event("startup") -async def on_startup(): - await init_db() @app.get("/health") async def health_check(): diff --git a/app/routes/mcp.py b/app/routes/mcp.py index 1e9b858..8a026ae 100644 --- a/app/routes/mcp.py +++ b/app/routes/mcp.py @@ -4,7 +4,10 @@ import contextvars from fastapi import APIRouter, Depends, Request, HTTPException from fastapi.responses import Response, JSONResponse 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 app.auth import get_current_agent 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"}))] -# FastMCP / SSE Integration -# connect_sse() ET handle_post_message() envoient chacun leur reponse ASGI -# completement par eux-memes (via le send() qu'on leur passe). Les faire -# passer par des routes FastAPI classiques (Depends + return Response) fait -# que FastAPI tente un second envoi une fois le SDK termine -> RuntimeError -# uvicorn ("Unexpected ASGI message 'http.response.start' sent, after response -# already completed"). Un seul montage ASGI brut gere GET (ouverture SSE) et -# POST (messages) sans jamais repasser par le wrapping FastAPI ; l'auth -# X-API-Key est donc verifiee ici a la main plutot que via Depends. -sse_transport = SseServerTransport("/messages") +# Streamable HTTP Integration (mcp SDK v1.x, transport recommande depuis la spec 2025-03-26). +# Remplace l ancien transport SSE (SseServerTransport) qui exigeait un session_id +# etabli via un GET prealable -- les clients MCP modernes (Gemini/Antigravity inclus) +# postent directement le JSON-RPC initialize sans cette poignee de main, d ou les 400 +# Bad Request observes avec l ancienne implementation. +# L auth X-API-Key reste verifiee a la main avant de deleguer a session_manager.handle_request, +# meme principe qu avant : current_agent_var est positionne pour la duree de la requete, +# lu ensuite par les handlers call_tool/list_tools via caller_agent. +session_manager = StreamableHTTPSessionManager( + app=mcp_server, + event_store=None, + json_response=True, + stateless=True, +) async def _mcp_asgi_app(scope, receive, send): if scope["type"] != "http": @@ -247,15 +254,10 @@ async def _mcp_asgi_app(scope, receive, send): return 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) - try: - await mcp_server.run(read_stream, write_stream, mcp_server.create_initialization_options()) - finally: - current_agent_var.reset(token) + token = current_agent_var.set(agent) + try: + await session_manager.handle_request(scope, receive, send) + finally: + current_agent_var.reset(token) router.mount("/mcp", _mcp_asgi_app) diff --git a/requirements.txt b/requirements.txt index b8a8535..dd69498 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ aiosqlite>=0.19.0 PyJWT>=2.8.0 jinja2>=3.1.3 python-multipart>=0.0.9 -mcp>=1.2.0 +mcp>=1.2.0,<2.0.0 python-dotenv>=1.0.1 sse-starlette>=1.6.5 +httpx>=0.27.0