fix(mcp+alembic): désactive DNS rebinding (421) + rechaîne migrations 006

MCP :
- FastMCP recevait Host=localhost (sans port) mais le pattern par défaut
  allowed_hosts=["localhost:*", ...] EXIGE un port → 421 Invalid Host header
  pour tout accès non-localhost (ex: Hermes via http://10.0.0.50:3001/mcp)
- Désactive enable_dns_rebinding_protection : le Bearer MCP_API_KEY est la
  vraie barrière (protection rebinding = anti-attaque navigateur, inutile ici)
- nginx /mcp : retour à Host $host (le rewrite localhost était cassé)

Alembic :
- Collision : 006_notes_urls et 006_product_tags partageaient revision='006'
  → "Multiple head revisions" au démarrage
- Renumérote notes_urls en 0061, chaîné après product_tags
  Chaîne finale : 005 -> 006 (product_tags) -> 0061 (notes_urls) -> 007

v0.5.15

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 10:07:00 +02:00
parent 7bf6caa3dd
commit 6c889f1561
5 changed files with 27 additions and 10 deletions
@@ -1,16 +1,19 @@
"""006 - ajout colonne urls (JSONB) sur notes.items
"""0061 - ajout colonne urls (JSONB) sur notes.items
Revision ID: 006
Revises: 005
Revision ID: 0061
Revises: 006
Create Date: 2026-05-30
Note : renumérotée 0061 (au lieu de 006) pour résoudre une collision avec
006_product_tags. Chaînée après product_tags : 005 -> 006 -> 0061 -> 007.
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSONB
revision = '006'
down_revision = '005'
revision = '0061'
down_revision = '006'
branch_labels = None
depends_on = None
@@ -1,7 +1,7 @@
"""007 - list_type sur shopping.lists, url/description/image_url sur list_items
Revision ID: 007
Revises: 006
Revises: 0061
Create Date: 2026-05-30
"""
@@ -9,7 +9,7 @@ from alembic import op
import sqlalchemy as sa
revision = '007'
down_revision = '006'
down_revision = '0061'
branch_labels = None
depends_on = None
+15 -1
View File
@@ -4,6 +4,7 @@ from datetime import datetime, timedelta, timezone, date as date_type
from decimal import Decimal
from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
from sqlalchemy import select, and_, text, or_
from sqlalchemy.orm import selectinload
@@ -15,7 +16,20 @@ from app.models.shopping import ShoppingList, ListItem, Product
_VALID_STATUSES = {"pending", "done", "cancelled"}
_VALID_PRIORITIES = {"low", "medium", "high"}
mcp = FastMCP("HomeHub", stateless_http=True, streamable_http_path="/")
# La protection DNS rebinding (défaut FastMCP) valide le header Host contre
# ["127.0.0.1:*", "localhost:*", "[::1]:*"]. Elle est conçue contre les attaques
# navigateur sur des services localhost. Ici l'accès se fait depuis des agents
# externes (Hermes) via l'IP du serveur, et la vraie barrière est le Bearer token
# MCP_API_KEY (cf. MCPAuthMiddleware). On désactive donc cette protection devenue
# redondante et bloquante (sinon 421 "Invalid Host header" sur toute IP non-localhost).
mcp = FastMCP(
"HomeHub",
stateless_http=True,
streamable_http_path="/",
transport_security=TransportSecuritySettings(
enable_dns_rebinding_protection=False,
),
)
def _serialize(obj):
+1 -1
View File
@@ -27,7 +27,7 @@ server {
location /mcp {
proxy_pass http://backend:8000/mcp;
proxy_set_header Host localhost;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Connection "";
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "homehub-frontend",
"private": true,
"version": "0.5.14",
"version": "0.5.15",
"type": "module",
"scripts": {
"dev": "vite",