diff --git a/backend/app/main.py b/backend/app/main.py index edc0b75..ea11fbe 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -7,17 +7,18 @@ from app.api.admin import router as admin_router from app.api.events import router as events_router from app.api.health import router as health_router from app.api.media import router as media_router +from app.api.mcp_server import mcp from app.api.notes import router as notes_router from app.api.todos import router as todos_router from app.api.shopping import router as shopping_router from app.core.config import settings +from app.core.mcp_auth import MCPAuthMiddleware from app.core.redis import init_redis, close_redis from app.data.seed import run_seed @asynccontextmanager async def lifespan(app: FastAPI): - # Crée les dossiers data/ au démarrage for subdir in ("uploads", "notes", "backup"): Path(settings.data_dir, subdir).mkdir(parents=True, exist_ok=True) await run_seed() @@ -35,6 +36,7 @@ app.add_middleware( allow_methods=["*"], allow_headers=["*"], ) +app.add_middleware(MCPAuthMiddleware) app.include_router(health_router, prefix="/api") app.include_router(events_router, prefix="/api/events") @@ -44,4 +46,5 @@ app.include_router(notes_router, prefix="/api/notes") app.include_router(todos_router, prefix="/api/todos") app.include_router(shopping_router, prefix="/api/shopping") +app.mount("/mcp", mcp.streamable_http_app()) app.mount("/media", StaticFiles(directory=str(settings.upload_path)), name="media") diff --git a/backend/tests/test_mcp.py b/backend/tests/test_mcp.py index 3651a0f..efb7588 100644 --- a/backend/tests/test_mcp.py +++ b/backend/tests/test_mcp.py @@ -3,6 +3,7 @@ import uuid import pytest from sqlalchemy import delete import app.api.mcp_server as mcp_server_module +from app.core.config import settings from app.api.mcp_server import ( get_todos, create_todo, update_todo, postpone_todo, delete_todo, ) @@ -211,3 +212,27 @@ async def test_add_item_liste_invalide(): result = await add_shopping_item(list_id="pas-un-uuid", name="article") data = json.loads(result) assert "error" in data + + +# ── AUTH ────────────────────────────────────────────────────────────────────── + +async def test_mcp_auth_rejet_sans_token(client): + """Le middleware renvoie 401 si aucun header Authorization.""" + resp = await client.get("/mcp") + assert resp.status_code == 401 + + +async def test_mcp_auth_rejet_mauvais_token(client): + """Le middleware renvoie 401 si le token est incorrect.""" + resp = await client.get("/mcp", headers={"Authorization": "Bearer mauvais-token"}) + assert resp.status_code == 401 + + +async def test_mcp_auth_accepte_bon_token(client, monkeypatch): + """Le middleware laisse passer avec le token correct.""" + monkeypatch.setattr(settings, "mcp_api_key", "test-mcp-key-xyz") + resp = await client.get( + "/mcp", + headers={"Authorization": "Bearer test-mcp-key-xyz"}, + ) + assert resp.status_code != 401 diff --git a/docker-compose.yml b/docker-compose.yml index 964cb99..7b61b27 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,7 @@ services: DATA_DIR: /data REDIS_URL: redis://redis:6379 CORS_ORIGINS: http://localhost:3001,http://localhost:3000 + MCP_API_KEY: "4zfCmiC3Z_28F3xPOxBSDi0DQx6aRzAQrpplyywo_VI" volumes: - ./backend/app:/app/app - ./data:/data diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 1197f68..2e99e20 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -25,6 +25,17 @@ server { proxy_read_timeout 86400s; } + location /mcp { + proxy_pass http://backend:8000/mcp; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 86400s; + } + location /api/ { proxy_pass http://backend:8000/api/; proxy_set_header Host $host;