5.7 KiB
Colonnes configurables pour rechercher_marches (MCP) — Design
Issue liée : connecteur MCP colibre (#114).
Objectif
Rendre la sélection des colonnes du tool MCP rechercher_marches souple, au lieu
de la liste figée MARCHES_COLUMNS. Trois besoins :
- Un choix par défaut (les colonnes actuelles), comportement inchangé si le client ne demande rien.
- Un champ de lien dynamique vers chaque marché, ajouté à chaque résultat :
APP_BASE_URL/marche/{uid}. - La possibilité pour l'agent/l'utilisateur de choisir d'autres colonnes parmi celles disponibles, avec la meilleure UX atteignable dans l'interface tool MCP.
Contexte UX (ce qui est possible, ce qui ne l'est pas)
- Cases à cocher rendues par le serveur : impossible. dash 4.4 n'implémente pas
l'élicitation MCP (le serveur n'annonce que les capacités
toolsetresources). Aucun widget interactif ne peut être poussé dans le client. - Levier retenu : un
enumdans le schéma du paramètre. En typantcolonnesavec unLiteraldes colonnes disponibles, l'agent reçoit la liste fermée valide directement dans le schéma du tool (pas de tâtonnement, pas besoin d'appelerschema_donneesau préalable). Beaucoup de clients (dont Claude) rendent un paramètre enum comme un sélecteur cochable. C'est aussi une validation au niveau schéma.
Source de vérité des colonnes
DATA_SCHEMA (issu du TableSchema base_schema.json) est la référence, déjà
utilisée par describe_schema(). L'ensemble sélectionnable part de l'intersection DATA_SCHEMA ∩ duckdb_schema
(exactement le set déjà exposé comme colonnes_filtrables), unie aux colonnes
du défaut MARCHES_COLUMNS — pour que toute colonne du jeu par défaut reste
re-sélectionnable même si elle est enrichie et absente de DATA_SCHEMA (ex.
acheteur_nom). Ces colonnes du défaut sont toutes présentes dans la table DuckDB
(elles fonctionnent déjà), donc sûres à SELECT :
_filtrables = tuple(name for name in DATA_SCHEMA if name in duckdb_schema)
SELECTABLE_COLUMNS = tuple(dict.fromkeys((*MARCHES_COLUMNS, *_filtrables)))
Source de vérité unique (schéma de référence + défaut), ni liste « raw DuckDB », ni sous-ensemble à maintenir à la main.
Modifications
src/mcp/queries.py
- Construire à l'import (cf. section « Source de vérité ») :
_filtrables = tuple(name for name in DATA_SCHEMA if name in duckdb_schema) SELECTABLE_COLUMNS = tuple(dict.fromkeys((*MARCHES_COLUMNS, *_filtrables))) ColonneMarche = Literal[SELECTABLE_COLUMNS] search_marches(..., colonnes: list[str] | None = None):colonnes is None→MARCHES_COLUMNS(comportement inchangé).- liste fournie → exactement ces colonnes (remplace le défaut).
- Validation runtime conservée (défense en profondeur : le
enumdu schéma n'est pas toujours imposé par le client). Toute colonne absente deSELECTABLE_COLUMNS→ retour{"error": "colonne inconnue: <col>", "champ": col}(même patron que les erreurs de filtre). C'est ce qui protège l'interpolation SQL brute dequery_marches(src/db.pyfait", ".join(columns)sans quoting ni validation, prévu pour des appelants internes seulement). uidtoujours récupéré en interne (nécessaire au lien) même s'il n'est pas demandé, et toujours présent en sortie (clé primaire).lientoujours ajouté à chaque marché après la requête (champ virtuel, calculé en Python comme la colonnemarche) :f"{base}/marche/{uid}"avecbase = os.getenv("APP_BASE_URL", "").rstrip("/")(cohérent avec le reste du code :src/mcp/auth.py,oauth/routes.py, etc.).
describe_schema(): ajoute la clécolonnes_disponibles = list(SELECTABLE_COLUMNS).lienest mentionné danscolonnes_retournees.
src/mcp/tools.py
rechercher_marches(..., colonnes: list[ColonneMarche] | None = None)— c'est cette annotationenumqui porte l'UX.- Docstring mise à jour : décrit
colonnes(défaut = jeu standard ; renvoie versschema_donnees().colonnes_disponibles), et mentionne le champlien.
Points notables / décisions
liennon désactivable (YAGNI). Toujours présent.APP_BASE_URLnon défini (dev) →lienrelatif/marche/{uid}. Acceptable (dev only), cohérent avec le fallback des autres modules.- Sémantique « remplace » (et non « ajoute ») :
colonnes=[...]renvoie exactement ce set (+uid+lien). Choix validé : l'utilisateur maîtrise précisément ce qu'il reçoit. - Pas de champ
titled'affichage pour les tools : abandonné (dash 4.4 n'a pas de titre séparé duname, qui sert à la fois d'identifiant et d'affichage).
Tests — tests/mcp/test_queries.py
colonnes=None→ renvoie le jeu par défaut (MARCHES_COLUMNS) inchangé,lienprésent.colonnes=["objet", "montant"]→ renvoie exactement ces colonnes +uid+lien.- Colonne invalide (
["nexiste_pas"]) →{"error": ..., "champ": "nexiste_pas"}, aucune requête SQL avec la colonne interpolée. lienbien formé :<APP_BASE_URL>/marche/<uid>(monkeypatchAPP_BASE_URL).uidprésent en sortie même absent decolonnes.describe_schema()exposecolonnes_disponiblesnon vide et surensemble decolonnes_filtrables(inclut les colonnes du défaut).- Le schéma du paramètre
colonnesdu toolrechercher_marchescontient bien unenum(viaTypeAdaptersur l'annotation, ou le builder dash).
Hors périmètre
- Titre d'affichage joli pour les tools (abandonné).
- Toute modification de l'API REST
/data. - Élicitation / widgets interactifs (non supportés par dash 4.4).