# 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//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//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 `), 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) 1. Reverse-proxy DSM : `baserow-schema.bolbol.tn` → `3101` (meme manip que `cin.bolbol.tn`/`formation.bolbol.tn`, pas d'automatisation connue depuis le NAS/Mac pour cette etape). 2. Ajouter le connecteur MCP personnalise cote Claude.ai avec l'URL `https://baserow-schema.bolbol.tn/mcp//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`.