Documente le nouveau service compagnon (create_table/delete_table/create_field/ delete_field) et les 4 pieges rencontres (JWT obligatoire sur ces routes, API bas niveau SseServerTransport instable entre versions mcp, DNS-rebinding protection allowed_hosts=[] par defaut, 409 transitoire apres link_row). Resync complet de common/ports-registry.md (plusieurs semaines de derive). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.3 KiB
baserow-schema-mcp — serveur MCP compagnon pour la gestion de schema Baserow (17/08/2026)
Statut : PRODUCTION (4 outils testes end-to-end). Reste une action manuelle :
reverse-proxy DSM baserow-schema.bolbol.tn → 3101, puis ajout du connecteur
cote Claude.ai.
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 deupdate_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 domainebaserow-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 reseaudefaultsupplementaire) — lecon deja tiree dansbaserow-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=True → tout 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).
Piege n°4 — 409 Conflict transitoire apres un champ link_row
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.
Reste a faire (action manuelle Nabil)
- Reverse-proxy DSM :
baserow-schema.bolbol.tn→3101(meme manip quecin.bolbol.tn/formation.bolbol.tn, pas d'automatisation connue depuis le NAS/Mac pour cette etape). - Ajouter le connecteur MCP personnalise cote Claude.ai avec l'URL
https://baserow-schema.bolbol.tn/mcp/<TOKEN>/sse(TOKEN =MCP_SECRET_TOKENdans/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.