Files
nas-runbooks/common/baserow-schema-mcp.md
Nabil DerouicheandClaude Sonnet 5 44edfe5a70 baserow-schema-mcp : blocage SSE DSM resolu (HTTP 1.0 backend), connecteur pret
Nabil a identifie le fix cote formulaire DSM (HTTP 1.0 au lieu de 1.1 en
version backend du reverse-proxy). Reteste avec succes en conditions
reelles : SSE + les 4 outils bout-en-bout via l'URL publique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 17:53:50 +01:00

8.7 KiB

MAJ 17/08/2026 (resolu) : reverse-proxy DSM cree, blocage SSE initial resolu en choisissant HTTP 1.0 (pas 1.1) comme version backend dans le formulaire DSM. Reteste avec succes en conditions reelles : SSE (curl h2 par defaut ET httpx http/1.1 pur), puis les 4 outils bout-en-bout (create_table, create_field, delete_field, delete_table) via l'URL publique. Connecteur Claude.ai pret a etre ajoute.

baserow-schema-mcp — serveur MCP compagnon pour la gestion de schema Baserow (17/08/2026)

Statut : PRODUCTION. 4 outils testes end-to-end en interne PUIS via l'URL publique reelle (https://baserow-schema.bolbol.tn/mcp/<token>/sse). Reste uniquement l'ajout du connecteur cote Claude.ai (action manuelle Nabil, pas d'API pour ca).

Probleme

Le MCP natif de Baserow (https://baserow.bolbol.tn/mcp/<token>/sse, cf. baserow-mcp-oauth-fix.md) expose uniquement des outils de LIGNES (list_databases, list_tables, get_table_schema, create_rows, update_rows, delete_rows, list_table_rows) — aucun outil de gestion de schema (creer/supprimer une table ou un champ). Baserow tourne en image officielle (baserow/baserow:latest, watchtower), non patchable directement (meme constat que baserow-mcp-oauth-fix.md) : impossible d'ajouter ces outils au MCP natif.

Solution

Nouveau service independant, meme famille que baserow-oauth-stub (sidecar OAuth rubber-stamp devant un vrai serveur), a /volume1/docker/baserow-schema-mcp/ (Gitea bolbol/baserow-schema-mcp, prive) :

  • baserow-schema-mcp (FastAPI-free, mcp.server.fastmcp.FastMCP, build local, pas de port publie) : 4 outils, perimetre strict, rien d'autre (pas de update_field, pas de gestion de databases/workspaces) :
    • create_table(database_id, name, data?, first_row_header?)POST /api/database/tables/database/{database_id}/
    • delete_table(table_id)DELETE /api/database/tables/{table_id}/
    • create_field(table_id, field)POST /api/database/fields/table/{table_id}/ (field = payload brut Baserow, passthrough)
    • delete_field(field_id)DELETE /api/database/fields/{field_id}/
  • baserow-schema-mcp-oauth-app + baserow-schema-mcp-proxy (nginx, port 3101→80) : copie conforme du sidecar OAuth de baserow-oauth-stub, adaptee au domaine baserow-schema.bolbol.tn. Rubber-stamp uniquement — la vraie protection est le token secret dans l'URL /mcp/<token>/sse (meme logique que Baserow natif).
  • Les 3 services sont uniquement sur le reseau externe n8n (pas de reseau default supplementaire) — lecon deja tiree dans baserow-mcp-oauth-fix.md, reappliquee directement ici sans la redecouvrir.

Piege n°1 — les 4 routes de schema n'acceptent PAS le Database API Token

Confirme via /api/redoc/ (schema OpenAPI complet, security par route) : POST /api/database/tables/database/{id}/, DELETE /api/database/tables/{id}/, DELETE /api/database/fields/{id}/ n'acceptent QUE JWT/UserSource JWT. Seul POST /api/database/fields/table/{id}/ (creation de champ) accepte aussi Database token — mais pas les 3 autres. Il faut donc un login email+mot de passe (POST /api/user/token-auth/, header ensuite Authorization: JWT <access_token>), pas le token Baserow simple utilise partout ailleurs sur cette infra (rla-api, gsparc-mezzouna, context-hub...).

Lien avec les incidents connus : dashboard-terrain et gsparc-mezzouna-api ont deja ete casses par un JWT Baserow cache indefiniment qui expire/rotate silencieusement (mot de passe ou BASEROW_JWT_SIGNING_KEY change), et le fix a chaque fois ete de fuir le JWT au profit d'un Database token statique. Ici c'est impossible (API Baserow l'exige). Mitigation dans app/baserow_client.py : re-login proactif toutes les 12h (moitie de l'expiration 24h du JWT Baserow) + re-login automatique et une seule retentative sur toute reponse 401, plutot que de laisser echouer silencieusement comme les incidents precedents.

Piege n°2 — API bas niveau mcp.server.sse.SseServerTransport non stable entre versions

Premiere version du code copiait le pattern SSE de bolbol/context-hub (transport.read_stream(), transport.write_stream(), transport.handle_sse(request)) — ces methodes n'existent plus dans mcp==1.29.0 (installe ici ; context-hub avait fige une version bien plus ancienne du meme mcp>=1.2.0,<2.0.0). Erreur en prod : AttributeError: 'SseServerTransport' object has no attribute 'handle_sse'.

L'API bas niveau actuelle expose connect_sse(scope, receive, send) (context manager async qui ecrit directement la reponse ASGI, incompatible avec un simple return EventSourceResponse(...) FastAPI) — fragile a cabler a la main. Fix retenu : passer par l'API haut niveau mcp.server.fastmcp.FastMCP (@mcp.tool() + mcp.sse_app()), maintenue par le SDK et deja correcte pour la version installee, plutot que de re-cabler le bas niveau a chaque bump de version. Le token secret est baked directement dans le chemin du Mount (Mount(f"/mcp/{TOKEN}", app=mcp.sse_app())), pas de validation manuelle necessaire.

A retenir pour tout futur service MCP maison sur cette infra : preferer FastMCP a la construction manuelle de Server + SseServerTransport — plus court ET plus stable dans le temps.

Piege n°3 — DNS-rebinding protection de mcp >=1.29 rejette tout par defaut

TransportSecuritySettings.allowed_hosts vaut [] par defaut avec enable_dns_rebinding_protection=Truetout Host header est rejete (421 Invalid Host header), y compris en interne. Fix : passer explicitement transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...]) a FastMCP(...) avec la liste exacte des Host attendus (domaine public + nom de service Docker:port pour les tests internes directs).

Creer un champ juste apres un link_row (qui declenche la creation du champ inverse dans la table liee, cf verrou table cote Baserow) peut renvoyer un 409 Conflict sur le create_field suivant immediat. Mitigation appliquee lors de la creation des tables CI-CPT 2026 : retry avec backoff (jusqu'a 6 tentatives, 2s/4s/6s...) + 1-1.5s de pause entre chaque create_field. Un seul 409 rencontre en pratique (table "Bons de Commande CI-CPT 2026", champ lots_couverts juste apres fournisseur), resolu au 1er retry.

Piege n°5 — reverse-proxy DSM : forcer HTTP 1.0 (pas 1.1) en version backend

Reverse-proxy DSM cree par Nabil pour baserow-schema.bolbol.tn3101, cert Let's Encrypt auto-provisionne OK, TLS OK — mais flux SSE bloque silencieusement (0 octet recu) tant que le formulaire DSM etait regle sur HTTP 1.1 pour la connexion backend. Fix : choisir HTTP 1.0 dans ce champ (pas 1.1). Une fois change, SSE fonctionne dans les deux sens observes : client curl par defaut (ALPN h2) ET client httpx pur HTTP/1.1 — donc pas de regression sur les clients non-h2 malgre le nom "HTTP 1.0" trompeur dans le libelle DSM (il s'agit vraisemblablement d'un mode de proxying moins strict sur le framing chunked/keep-alive cote DSM->backend, pas d'une vraie retrogradation du protocole cote client). A appliquer directement sur toute future regle DSM devant un service SSE/MCP si le meme blocage silencieux apparait, avant de chercher plus loin.

Reste a faire (action manuelle Nabil)

  1. Reverse-proxy DSM : baserow-schema.bolbol.tn3101 FAIT 17/08/2026 (voir piege n°5 ci-dessus pour le reglage HTTP 1.0).
  2. Ajouter le connecteur MCP personnalise cote Claude.ai avec l'URL https://baserow-schema.bolbol.tn/mcp/<TOKEN>/sse (TOKEN = MCP_SECRET_TOKEN dans /volume1/docker/baserow-schema-mcp/.env, non commite sur Gitea).

Fichiers

/volume1/docker/baserow-schema-mcp/ : app/main.py (FastMCP + 4 tools), app/baserow_client.py (JWT client), oauth_app.py/nginx.conf/ docker-compose.yml (sidecar OAuth, copie du pattern baserow-oauth-stub adaptee). Depot Gitea bolbol/baserow-schema-mcp (prive, .env gitignore). Entree ports-registry.md (port 3101) et miroir common/ports-registry.md a mettre a jour dans la meme session.

Usage — CI-CPT 2026 (premier cas d'usage reel)

3 tables creees dans la base "Zone Sud Achats" (id 229) via ces 4 outils : Bons de Commande CI-CPT 2026 (id 1090), Articles CI-CPT 2026 (id 1091), Répartition régionale CI-CPT 2026 (id 1092). Astuce utilisee pour nommer le champ primaire sans PATCH separe : create_table(..., data=[["nom_du_champ"]], first_row_header=true) — cree une table avec un seul champ primaire texte nomme comme voulu, sans les champs d'exemple par defaut (Name/Notes/Active). 15 tables obsoletes supprimees dans la foulee (AO 19/2026, Reglement definitif, AO 27/2026 — sous-ensemble confirme) avec delete_table.