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>
157 lines
8.7 KiB
Markdown
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`.
|