Files
nas-runbooks/common/baserow-schema-mcp.md
T
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

157 lines
8.7 KiB
Markdown

**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=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.
## 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.tn``3101`,
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.tn` → `3101`~~ 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`.