Runbook baserow-schema-mcp : 4 outils de gestion de schema Baserow
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a097f7def4
commit
2d8ed89502
@@ -0,0 +1,134 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user