{
  "name": "bot.ecorce.app",
  "version": "1",
  "status": "brouillon",
  "base": "https://bot.ecorce.app",
  "auth": {
    "type": "bearer",
    "how": "https://bot.ecorce.app/start.txt",
    "token_prefix": "ec_act_"
  },
  "levels": {
    "read": "lecture (publique, limitée en débit)",
    "prepare": "préparer (jeton d’action d’une personne ; brouillons seulement)",
    "commit": "confirmer (humain seulement, jamais un bot)"
  },
  "order_states": [
    "draft",
    "confirmed",
    "sent",
    "acknowledged"
  ],
  "caps": {
    "maxQtyPerLine": 50,
    "maxLines": 40,
    "maxUnits": 400,
    "maxNote": 300
  },
  "core": [
    {
      "method": "GET",
      "path": "/llms.txt",
      "description": "ce résumé pour LLM"
    },
    {
      "method": "GET",
      "path": "/start.txt",
      "description": "démarrage pas à pas"
    },
    {
      "method": "GET",
      "path": "/docs",
      "description": "documentation (HTML) ; /docs.md en Markdown"
    },
    {
      "method": "GET",
      "path": "/api/v1",
      "description": "catalogue JSON des composants et outils"
    },
    {
      "method": "GET",
      "path": "/openapi.json",
      "description": "OpenAPI 3.1"
    },
    {
      "method": "POST",
      "path": "/mcp",
      "description": "serveur MCP (JSON-RPC : initialize, tools/list, tools/call)"
    },
    {
      "method": "POST",
      "path": "/api/v1/connect",
      "description": "démarre la connexion (device code) ; la personne approuve sur /c/{user_code}"
    },
    {
      "method": "POST",
      "path": "/api/v1/connect/token",
      "description": "récupère le jeton d’action une fois approuvé"
    },
    {
      "method": "DELETE",
      "path": "/api/v1/tokens/me",
      "description": "révoque le jeton avec lequel tu appelles"
    },
    {
      "method": "GET",
      "path": "/api/v1/health",
      "description": "état du service"
    }
  ],
  "components": [
    {
      "id": "products",
      "title": "Produits : recherche en langage naturel (pivot SPN)",
      "description": "Une recherche en français ou en anglais au lieu de 1 500 lignes : la liste fournisseur est traduite en articles SPN (Produit | Variété | Origine | Catégorie | Taille | Emballage | Méthode), regroupés par produit (produit + variété + bio). Chaque produit montre l’offre la moins chère, l’écart de prix, les origines, le bio et un best_fit expliqué. Les commandes peuvent s’exprimer en articles SPN avec une quantité approximative ; resolve_to_supplier les retraduit en lignes du fournisseur avec les raisons. Prix au 5 août 2025, données de test. Ni actuels ni engageants. / Prices as of August 5, 2025, test data. Not current, not binding.",
      "upstream": {
        "name": "pivot SPN (src/data/pivot.js) + liste Canadawide chargée",
        "mode": "généré par scripts/normalise_pricelist.mjs ; SPN de base du 2026-08-27 ; prix de test"
      },
      "knowledge": [
        {
          "path": "/kb/commande.md",
          "title": "Comment fonctionne la commande F&L (inclut le SPN)"
        }
      ],
      "tools": [
        {
          "name": "search_products",
          "level": "read",
          "description": "Cherche des produits en langage naturel (FR/EN), par ex. « carottes bio du Québec, environ 10 caisses », « apples from Ontario under $30 », « 20 kg d’oranges navel ». La requête devient des filtres (produit, variété, bio, origine, quantité approximative, prix max) ; les champs explicites ont priorité. Classement : pertinence, puis prix (ou total estimé si une quantité est donnée). Chaque résultat a best_fit (offre choisie + reasons) et, avec une quantité, packs_needed et est_total_cad. Aucune conversion d’unité n’est inventée : si le poids ou le nombre par format n’est pas écrit, la réponse le dit. « local », « du coin », « locally grown » (ou proximity=true) ne filtre pas : les offres sont classées par proximité (origin_tier : Québec 1, reste du Canada 2, États-Unis 3, ailleurs 4 ; code ambigu comme CA, NL, PE = niveau non confirmé avec une plage, placé après), puis par prix. Une origine explicite (« du Québec ») reste un filtre strict. Un prix très bas par rapport au même produit et format porte price_check « prix à vérifier » et n’est pas choisi comme best_fit sans que ce soit dit. Sans requête (ou level=family) : la liste des familles. Prix au 5 août 2025, données de test. Ni actuels ni engageants. / Prices as of August 5, 2025, test data. Not current, not binding.",
          "input_schema": {
            "type": "object",
            "example": {
              "query": "carottes bio du Québec, environ 10 caisses"
            },
            "properties": {
              "query": {
                "type": "string",
                "maxLength": 200
              },
              "category": {
                "type": "string",
                "enum": [
                  "fruits",
                  "legumes",
                  "autres"
                ]
              },
              "organic": {
                "type": "boolean"
              },
              "origin": {
                "type": "string",
                "maxLength": 40,
                "description": "ex. Québec, Canada, Mexique, US, California"
              },
              "proximity": {
                "type": "boolean",
                "description": "classer par proximité (comme « local ») au lieu de filtrer"
              },
              "max_price_cad": {
                "type": "number",
                "exclusiveMinimum": 0,
                "maximum": 100000
              },
              "quantity": {
                "type": "string",
                "maxLength": 40,
                "description": "ex. « 10 caisses », « 20 kg », « 50 lb », « 200 unités »"
              },
              "limit": {
                "type": "integer",
                "minimum": 1,
                "maximum": 50,
                "default": 8
              },
              "offset": {
                "type": "integer",
                "minimum": 0,
                "maximum": 5000,
                "default": 0
              },
              "level": {
                "type": "string",
                "enum": [
                  "product",
                  "family"
                ]
              },
              "lang": {
                "type": "string",
                "enum": [
                  "fr",
                  "en"
                ],
                "description": "Langue des libellés et raisons (sinon détectée dans la requête). / Language of labels and reasons (detected from the query otherwise)."
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/search_products"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "get_product",
          "level": "read",
          "description": "Un produit (produit + variété + bio) avec toutes ses offres (une par article SPN : origine, taille, format) et, sous chaque offre, les lignes du fournisseur (sku, description, prix, correspondance : confidence et source). Prix au 5 août 2025, données de test. Ni actuels ni engageants. / Prices as of August 5, 2025, test data. Not current, not binding.",
          "input_schema": {
            "type": "object",
            "required": [
              "product_id"
            ],
            "example": {
              "product_id": "p_20107804"
            },
            "properties": {
              "product_id": {
                "type": "string",
                "pattern": "^p_[a-f0-9]{8}$"
              },
              "lang": {
                "type": "string",
                "enum": [
                  "fr",
                  "en"
                ],
                "description": "Langue des libellés et raisons (sinon détectée dans la requête). / Language of labels and reasons (detected from the query otherwise)."
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/get_product"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "get_spn_item",
          "level": "read",
          "description": "Un article SPN (7 champs) et les lignes du fournisseur qui y correspondent, avec la confiance de chaque correspondance (high|medium|low), sa source (spn|rules|ai|manual), la confiance de l’origine et du format. Sert à vérifier et corriger la table de correspondance.",
          "input_schema": {
            "type": "object",
            "required": [
              "spn_id"
            ],
            "example": {
              "spn_id": "s_309883c494"
            },
            "properties": {
              "spn_id": {
                "type": "string",
                "pattern": "^s_[a-f0-9]{10}$"
              },
              "lang": {
                "type": "string",
                "enum": [
                  "fr",
                  "en"
                ],
                "description": "Langue des libellés et raisons (sinon détectée dans la requête). / Language of labels and reasons (detected from the query otherwise)."
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/get_spn_item"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "suggest_substitutes",
          "level": "read",
          "description": "Solutions de rechange proches quand le produit voulu dépasse le budget, n’a pas la bonne origine ou n’existe pas en bio : même produit autre offre, même variété bio ou non, autre variété. Chaque suggestion dit ce qu’elle respecte (why) et ce qu’elle ne respecte pas (trade_offs), avec l’écart de prix. Donne product_id ou une requête. Prix au 5 août 2025, données de test. Ni actuels ni engageants. / Prices as of August 5, 2025, test data. Not current, not binding.",
          "input_schema": {
            "type": "object",
            "example": {
              "query": "laitue romaine bio de Californie"
            },
            "properties": {
              "product_id": {
                "type": "string",
                "pattern": "^p_[a-f0-9]{8}$"
              },
              "query": {
                "type": "string",
                "maxLength": 200
              },
              "organic": {
                "type": "boolean"
              },
              "origin": {
                "type": "string",
                "maxLength": 40
              },
              "max_price_cad": {
                "type": "number",
                "exclusiveMinimum": 0,
                "maximum": 100000
              },
              "limit": {
                "type": "integer",
                "minimum": 1,
                "maximum": 10,
                "default": 6
              },
              "lang": {
                "type": "string",
                "enum": [
                  "fr",
                  "en"
                ],
                "description": "Langue des libellés et raisons (sinon détectée dans la requête). / Language of labels and reasons (detected from the query otherwise)."
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/suggest_substitutes"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "resolve_to_supplier",
          "level": "read",
          "description": "Retraduit une commande exprimée en articles SPN (une ou plusieurs épiceries : buyer) en lignes réelles du fournisseur : regroupe par article SPN, choisit la ligne au total le plus bas qui couvre la quantité (arrondi à l’unité de vente supérieure), donne la raison, les solutions de rechange et la répartition par buyer. Une quantité en kg, lb ou unités n’est convertie que si le format l’indique ; sinon la ligne reste unresolved avec la raison. Calcul seulement : rien n’est commandé. Prix au 5 août 2025, données de test. Ni actuels ni engageants. / Prices as of August 5, 2025, test data. Not current, not binding.",
          "input_schema": {
            "type": "object",
            "required": [
              "lines"
            ],
            "example": {
              "lines": [
                {
                  "spn_id": "s_309883c494",
                  "qty": 2,
                  "buyer": "epicerie-a"
                },
                {
                  "spn_id": "s_309883c494",
                  "amount": 15,
                  "unit": "kg",
                  "buyer": "epicerie-b"
                }
              ]
            },
            "properties": {
              "lines": {
                "type": "array",
                "maxItems": 200,
                "items": {
                  "type": "object",
                  "properties": {
                    "spn_id": {
                      "type": "string",
                      "pattern": "^s_[a-f0-9]{10}$"
                    },
                    "sku": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 40
                    },
                    "product_id": {
                      "type": "string",
                      "pattern": "^p_[a-f0-9]{8}$"
                    },
                    "offer": {
                      "type": "string",
                      "maxLength": 40,
                      "description": "Avec product_id : spn_id d’une offre, sku d’une ligne, ou « best » (défaut : le total estimé le plus bas). / With product_id: an offer spn_id, a line sku, or \"best\"."
                    },
                    "qty": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 1000,
                      "description": "Unités de vente (caisses, sacs… telles que décrites). / Sell units."
                    },
                    "amount": {
                      "type": "number",
                      "exclusiveMinimum": 0,
                      "maximum": 100000,
                      "description": "Quantité approximative, avec unit. / Approximate amount, with unit."
                    },
                    "unit": {
                      "type": "string",
                      "enum": [
                        "pack",
                        "kg",
                        "lb",
                        "count"
                      ],
                      "description": "pack = unité de vente ; kg et lb seulement si le poids du format est connu ; count seulement si le nombre d’unités est connu."
                    },
                    "buyer": {
                      "type": "string",
                      "maxLength": 40,
                      "description": "Qui demande (achat groupé) : sert à répartir les unités de vente. / Who asks (group buy)."
                    }
                  }
                }
              },
              "flex": {
                "type": "string",
                "enum": [
                  "pack",
                  "exact"
                ],
                "default": "pack",
                "description": "pack : une demande en kg/lb/unités peut être servie par le même produit et la même origine dans un autre format. exact : seulement les lignes de l’article SPN."
              },
              "lang": {
                "type": "string",
                "enum": [
                  "fr",
                  "en"
                ],
                "description": "Langue des libellés et raisons (sinon détectée dans la requête). / Language of labels and reasons (detected from the query otherwise)."
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/resolve_to_supplier"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "list_mappings",
          "level": "read",
          "description": "Table de correspondance ligne fournisseur → article SPN, pour la relire : filtre par confidence (high|medium|low), source (spn|rules|ai|manual), q (texte). Chaque ligne a supplier_id, sku, description, spn_id, spn, confidence, source, origin_confidence et unit_confidence. Corrections : data/mapping-overrides.json (source manual), puis on relance la normalisation. Pas de prix ici.",
          "input_schema": {
            "type": "object",
            "properties": {
              "confidence": {
                "type": "string",
                "enum": [
                  "high",
                  "medium",
                  "low"
                ]
              },
              "source": {
                "type": "string",
                "enum": [
                  "spn",
                  "rules",
                  "ai",
                  "manual"
                ]
              },
              "q": {
                "type": "string",
                "maxLength": 80
              },
              "limit": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100,
                "default": 25
              },
              "offset": {
                "type": "integer",
                "minimum": 0,
                "maximum": 5000,
                "default": 0
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/list_mappings"
          },
          "callable_by_bot": true,
          "mcp": true
        }
      ]
    },
    {
      "id": "fl",
      "title": "Commande de fruits et légumes",
      "description": "Le cycle hebdomadaire : catalogue (liste Canadawide, prix au 5 août 2025, données de test), panier avec totaux en CAD, brouillon de commande, geste de l’épicier. Un bot prépare ; seul l’épicier confirme, et rien n’est envoyé à un distributeur (v1). Les prix sont des données de test : ni actuels ni engageants.",
      "upstream": {
        "name": "liste Canadawide chargée (xlsx)",
        "mode": "données de test, prix au 5 août 2025, données de test, prix publics, rechargeable par script ; ecorce-commande (réel, sans prix) seulement pour une personne avec un jeton de commande",
        "real_url": "https://ecorce-commande.simon-856.workers.dev"
      },
      "knowledge": [
        {
          "path": "/kb/commande.md",
          "title": "Comment fonctionne la commande F&L"
        }
      ],
      "tools": [
        {
          "name": "list_catalogue",
          "level": "read",
          "description": "Liste le catalogue de fruits et légumes de la liste Canadawide, avec les prix. Prix au 5 août 2025, données de test. Ni actuels ni engageants. / Prices as of August 5, 2025, test data. Not current, not binding. Chaque article a son sku (le code Canadawide), sa description, son nom SPN quand il existe, et price_cad avec prices_as_of. Filtres : q (texte), category (fruits|legumes|autres), bio, limit, offset.",
          "input_schema": {
            "type": "object",
            "properties": {
              "q": {
                "type": "string",
                "maxLength": 80
              },
              "category": {
                "type": "string",
                "enum": [
                  "fruits",
                  "legumes",
                  "autres"
                ]
              },
              "bio": {
                "type": "boolean"
              },
              "limit": {
                "type": "integer",
                "minimum": 1,
                "maximum": 50,
                "default": 20
              },
              "offset": {
                "type": "integer",
                "minimum": 0,
                "maximum": 5000,
                "default": 0
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/list_catalogue"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "get_cart",
          "level": "prepare",
          "description": "Lit ton panier courant (lignes sku + quantité) avec prix unitaire, total par ligne et sous-total en CAD. Prix au 5 août 2025, données de test. Ni actuels ni engageants. / Prices as of August 5, 2025, test data. Not current, not binding.",
          "input_schema": {
            "type": "object",
            "properties": {}
          },
          "endpoint": {
            "method": "POST",
            "path": "/api/v1/tools/get_cart"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "set_cart",
          "level": "prepare",
          "description": "Remplace tout le panier. Chaque ligne est : {sku, qty} (une ligne précise du fournisseur), {spn_id, qty} ou {spn_id, amount, unit} (un article SPN, quantité approximative : la ligne du fournisseur est choisie par resolve_to_supplier), ou {product_id, offer?, qty | amount + unit} (offer : spn_id, sku ou « best »). Plafonds après résolution : 50 unités de vente par ligne, 40 lignes, 400 unités en tout. Aucun champ de prix accepté : les prix viennent seulement de la liste chargée. Renvoie le panier résolu (ligne choisie, raison) avec les totaux en CAD (Prix au 5 août 2025, données de test). Ça ne commande rien.",
          "input_schema": {
            "type": "object",
            "required": [
              "lines"
            ],
            "example": {
              "lines": [
                {
                  "sku": "0201-3010-0012",
                  "qty": 2
                },
                {
                  "spn_id": "s_309883c494",
                  "amount": 20,
                  "unit": "kg"
                }
              ]
            },
            "properties": {
              "lines": {
                "type": "array",
                "maxItems": 40,
                "items": {
                  "type": "object",
                  "properties": {
                    "sku": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 40
                    },
                    "spn_id": {
                      "type": "string",
                      "pattern": "^s_[a-f0-9]{10}$"
                    },
                    "product_id": {
                      "type": "string",
                      "pattern": "^p_[a-f0-9]{8}$"
                    },
                    "offer": {
                      "type": "string",
                      "maxLength": 40
                    },
                    "qty": {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 50
                    },
                    "amount": {
                      "type": "number",
                      "exclusiveMinimum": 0,
                      "maximum": 5000
                    },
                    "unit": {
                      "type": "string",
                      "enum": [
                        "pack",
                        "kg",
                        "lb",
                        "count"
                      ]
                    }
                  }
                }
              },
              "notes": {
                "type": "string",
                "maxLength": 300,
                "default": ""
              }
            }
          },
          "endpoint": {
            "method": "POST",
            "path": "/api/v1/tools/set_cart"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "create_draft_order",
          "level": "prepare",
          "description": "Crée un BROUILLON de commande avec le panier courant (prix de la liste figés dans le brouillon, Prix au 5 août 2025, données de test) et renvoie un lien d’approbation /ok/{code} à envoyer à l’épicier. Ça ne passe jamais la commande : seul le geste de l’épicier la confirme, et même alors rien n’est envoyé à un distributeur.",
          "input_schema": {
            "type": "object",
            "properties": {
              "note": {
                "type": "string",
                "maxLength": 300
              }
            }
          },
          "endpoint": {
            "method": "POST",
            "path": "/api/v1/tools/create_draft_order"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "get_order_status",
          "level": "prepare",
          "description": "Donne l’état d’une commande (la dernière si order_id est omis) : draft, confirmed, sent ou acknowledged (ou cancelled). Après confirmation, renvoie le YAML à vérifier.",
          "input_schema": {
            "type": "object",
            "properties": {
              "order_id": {
                "type": "string",
                "pattern": "^ord_[a-f0-9]{12}$"
              }
            }
          },
          "endpoint": {
            "method": "POST",
            "path": "/api/v1/tools/get_order_status"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "confirm_order",
          "level": "commit",
          "description": "Confirme un brouillon. RÉSERVÉ À L’HUMAIN : lancé uniquement par le geste de l’épicier sur /ok/{code}. Marque la commande confirmed et produit le YAML à vérifier. N’envoie rien à un distributeur.",
          "input_schema": {
            "type": "object",
            "required": [
              "order_id"
            ],
            "properties": {
              "order_id": {
                "type": "string",
                "pattern": "^ord_[a-f0-9]{12}$"
              }
            }
          },
          "endpoint": null,
          "callable_by_bot": false,
          "mcp": false
        }
      ]
    },
    {
      "id": "spn",
      "title": "SPN : la norme de noms des fruits et légumes",
      "description": "Lecture seule. Cherche un produit dans la table de conversion Canadawide → SPN publiée par Écorce.",
      "upstream": {
        "name": "ecorce-spn",
        "mode": "réel, public, lecture seule",
        "real_url": "https://ecorce-spn.simon-856.workers.dev"
      },
      "knowledge": [
        {
          "path": "/kb/commande.md",
          "title": "Comment fonctionne la commande F&L (inclut le SPN)"
        }
      ],
      "tools": [
        {
          "name": "spn_search",
          "level": "read",
          "description": "Cherche des produits dans le SPN par texte libre (nom SPN, libellé Canadawide, SKU, marque). Renvoie le nom SPN en 7 champs. Le SPN ne contient pas de prix (ils sont dans list_catalogue, avec leur date).",
          "input_schema": {
            "type": "object",
            "required": [
              "q"
            ],
            "properties": {
              "q": {
                "type": "string",
                "minLength": 1,
                "maxLength": 80
              },
              "limit": {
                "type": "integer",
                "minimum": 1,
                "maximum": 25,
                "default": 10
              },
              "in_scope_only": {
                "type": "boolean",
                "default": true
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/spn_search"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "spn_lookup",
          "level": "read",
          "description": "Trouve UN produit du SPN par SKU Canadawide exact (sku) ou par libellé Canadawide exact (name). Donne les 7 champs du nom SPN.",
          "input_schema": {
            "type": "object",
            "properties": {
              "sku": {
                "type": "string",
                "maxLength": 40
              },
              "name": {
                "type": "string",
                "maxLength": 120
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/spn_lookup"
          },
          "callable_by_bot": true,
          "mcp": true
        }
      ]
    },
    {
      "id": "kb",
      "title": "Connaissances : Écorce, PivoHub, achats groupés",
      "description": "Courts documents publics en français (BROUILLON, à valider par Simon). Les outils ask_* cherchent dedans ; aucune donnée confidentielle, aucun CRM, aucune condition de contrat.",
      "upstream": {
        "name": "kb/*.md dans le Worker",
        "mode": "statique, versionné, brouillon"
      },
      "knowledge": [
        {
          "path": "/kb/achats-groupes.md",
          "title": "Achats groupés"
        },
        {
          "path": "/kb/commande.md",
          "title": "Commande de fruits et légumes"
        },
        {
          "path": "/kb/ecorce.md",
          "title": "Écorce"
        },
        {
          "path": "/kb/pivohub.md",
          "title": "PivoHub"
        }
      ],
      "tools": [
        {
          "name": "ask_ecorce",
          "level": "read",
          "description": "Pose une question sur Écorce (qui, quoi, chiffres publics, rythme des commandes). Répond avec des extraits du pack de connaissances (brouillon).",
          "input_schema": {
            "type": "object",
            "required": [
              "question"
            ],
            "properties": {
              "question": {
                "type": "string",
                "minLength": 3,
                "maxLength": 300
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/ask_ecorce"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "ask_pivohub",
          "level": "read",
          "description": "Pose une question sur PivoHub (son rôle auprès d’Écorce). Répond avec des extraits du pack de connaissances (brouillon).",
          "input_schema": {
            "type": "object",
            "required": [
              "question"
            ],
            "properties": {
              "question": {
                "type": "string",
                "minLength": 3,
                "maxLength": 300
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/ask_pivohub"
          },
          "callable_by_bot": true,
          "mcp": true
        },
        {
          "name": "group_buying_info",
          "level": "read",
          "description": "Explique les achats groupés (commandes mises en commun). La question est facultative : sans elle, renvoie le résumé.",
          "input_schema": {
            "type": "object",
            "properties": {
              "question": {
                "type": "string",
                "maxLength": 300,
                "default": "achats groupés"
              }
            }
          },
          "endpoint": {
            "method": "GET|POST",
            "path": "/api/v1/tools/group_buying_info"
          },
          "callable_by_bot": true,
          "mcp": true
        }
      ]
    }
  ]
}