Files
nas-runbooks/common/baserow-schema-mcp.md
T
Nabil DerouicheandClaude Sonnet 5 f3d8fae20b baserow-schema-mcp : piege HTTP/2 sur le reverse-proxy DSM
Le flux SSE reste bloque quand le client negocie HTTP/2 via ALPN sur la
nouvelle regle DSM baserow-schema.bolbol.tn -- fonctionne en HTTP/1.1 force.
Cause probable cote DSM (config divergente de baserow.bolbol.tn qui marche).

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

137 lines
7.5 KiB
Markdown

**MAJ 17/08/2026** : DSM cree, endpoint accessible en HTTPS mais flux SSE bloque en HTTP/2 (voir piege n°5 en fin de fichier PROTOCOL-INFRA.md) -- fonctionne en forcant HTTP/1.1. A verifier avant test connecteur Claude.ai.
# 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 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.
## 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/<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`.