20. Serveur MCP : les assistants IA sur Miraj
Le serveur MCP de Miraj est un point d'accès intégré à miraj-server qui permet aux assistants IA compatibles avec le Model Context Protocol de consulter, lire, modifier ou administrer des bases. L'accès repose sur un jeton créé en SQL, lié à un compte et plafonné à un niveau d'accès. Il est servi en HTTP sur le port 7008 et désactivé tant que vous ne l'activez pas.
Le serveur MCP de Miraj permet à un assistant d'intelligence artificielle (Claude, ChatGPT, DeepSeek ou tout client du protocole MCP, Model Context Protocol) de travailler sur les bases d'un serveur Miraj : consulter leur structure, lire les données, les modifier ou administrer le serveur, selon ce que vous lui accordez. L'assistant n'a besoin ni de pilote ni de mot de passe : il reçoit un jeton créé en SQL, lié à un compte et plafonné à un niveau d'accès.
Le point d'accès MCP est intégré à miraj-server : c'est le même processus et le même moteur que le port principal (7007), sur un port HTTP distinct (7008 par défaut), désactivé tant que vous ne l'ouvrez pas.
Assistant IA (Claude, ChatGPT, DeepSeek…)
│ JSON-RPC 2.0 sur HTTP ou HTTPS : POST http(s)://hôte:7008/mcp
│ Authorization: Bearer mjt_…
▼
┌──────────────────────────── miraj-server ────────────────────────────┐
│ port 7007 : protocole réseau de Miraj (applications, miraj-cli, …) │
│ port 7008 : point d'accès MCP (--mcp ON) │
│ jeton ─► compte ─► session Miraj plafonnée au niveau du jeton │
│ outils ─► instructions SQL ─► privilèges, verrous, déclencheurs, │
│ clés étrangères, journal, transactions, réplication │
└──────────────────────────────────────────────────────────────────────┘Tout passe par le moteur comme une instruction SQL ordinaire : privilèges du compte, verrous, contraintes CHECK, clés étrangères, déclencheurs, journal d'écriture et réplication s'appliquent exactement comme pour une application cliente. Rien n'est jamais écrit directement dans les fichiers de données.
20.1 À quoi sert le serveur MCP ?#
MCP est un protocole ouvert par lequel un assistant IA découvre des outils (fonctions qu'il peut appeler, avec leurs arguments décrits en JSON) et des ressources (documents qu'il peut lire). Miraj en expose seize :
- des outils de structure : lister les bases et leurs objets, décrire une table, lire une définition
CREATE, chercher une table ou une colonne par son nom ; - des outils de lecture : lire des lignes avec une condition structurée, lire une ligne par sa clé primaire, compter, afficher un plan d'exécution ;
- des outils d'écriture : insérer, modifier, supprimer des lignes ;
- des outils d'administration : créer une base ou une table, supprimer un objet ;
- un outil SQL libre,
execute_sql, pour tout le reste.
Les outils structurés sont préférables au SQL libre pour les opérations courantes : l'assistant fournit des noms de colonnes et des valeurs JSON, jamais du texte SQL ; les noms sont vérifiés contre la table et les valeurs deviennent des littéraux, ce qui rend toute injection impossible.
Usages typiques : laisser un assistant de développement explorer le schéma d'une application, écrire et vérifier des requêtes, préparer des données de test, diagnostiquer un plan d'exécution, ou, avec un jeton ADMIN sur une base de travail, créer lui-même les tables d'un prototype.
20.2 Quelles éditions incluent le serveur MCP ?#
| Édition | Point d'accès MCP |
|---|---|
| Entreprise | Disponible |
| Cluster | Disponible |
| Developer | Disponible (avec les limites propres à l'édition : un cœur, 20 Go, 24 heures) |
| Express | Non disponible : avec mcp = ON, le serveur démarre, affiche un avertissement et n'ouvre pas le port MCP ; CREATE MCP TOKEN est refusé (erreur 9001 « MCP n'est pas disponible dans l'édition Miraj Express ») |
20.3 Comment activer et configurer le serveur MCP ?#
20.3.1 Variables#
Le point d'accès se règle comme le port principal : valeur par défaut, puis fichier miraj_config.xml, puis ligne de commande, la ligne de commande l'emportant (voir 11.2). Il est désactivé par défaut.
Variable (miraj_config.xml) | Argument | Défaut | Valeurs | Rôle |
|---|---|---|---|---|
mcp | --mcp ON|OFF | OFF | ON, OFF (aussi true, false, 1, 0) | Ouvre ou non le point d'accès |
mcp_port | --mcp-port <port> | 7008 | 0 à 65535 | Port TCP du point d'accès, distinct de port ; 0 : port choisi par le système, affiché au démarrage |
mcp_bind | --mcp-bind <adresse> | 127.0.0.1 | adresse IP ou nom | Adresse d'écoute, indépendante de bind ; hors de la boucle locale, TLS obligatoire (20.3.5) |
mcp_idle_timeout | --mcp-idle-timeout <s> | 300 | 1 à 31 536 000 secondes | Une session MCP restée sans requête pendant ce délai est fermée ; sa transaction ouverte est annulée |
mcp_statement_timeout | --mcp-statement-timeout <s> | 30 | 0 à 31 536 000 secondes, fraction permise (2.5) ; 0 : sans limite | Durée maximale d'un appel d'outil (erreurs 1969 ou 1317 au-delà, voir 20.13) |
mcp_max_rows | --mcp-max-rows <n> | 5000 | 1 à 1 000 000 | Plafond des lignes rendues par un résultat, quel que soit le limit demandé |
La boucle locale s'écrit exactement 127.0.0.1, localhost ou ::1 ; toute autre adresse, y compris 0.0.0.0 (toutes les interfaces), est considérée comme publique.
Les variables mcp* figurent dans miraj_default.xml (régénéré à chaque démarrage) et dans le groupe MCP de l'éditeur graphique de configuration, qui signale un mcp_port égal à port et un mcp_bind public sans certificat.
20.3.2 Par le fichier miraj_config.xml#
<?xml version="1.0" encoding="UTF-8"?>
<miraj>
<!-- Point d'accès MCP ouvert (assistants IA, jetons CREATE MCP TOKEN) -->
<mcp>ON</mcp>
<mcp_port>7008</mcp_port>
<mcp_bind>127.0.0.1</mcp_bind>
<mcp_idle_timeout>300</mcp_idle_timeout>
<mcp_statement_timeout>30</mcp_statement_timeout>
<mcp_max_rows>5000</mcp_max_rows>
</miraj>20.3.3 Par la ligne de commande#
miraj-server.exe --root D:\miraj\data --log --mcp ON
miraj-server.exe --root D:\miraj\data --log --mcp ON --mcp-port 7100 --mcp-max-rows 1000Démarrez toujours le serveur avec --log : les ouvertures de session, les appels d'outils et les refus sont alors inscrits (20.13). Un argument l'emporte sur le fichier dans les deux sens : --mcp OFF ferme un point d'accès activé par le fichier, --mcp-port 0 remplace le port du fichier.
20.3.4 Messages de démarrage et erreurs de configuration#
| Situation | Message | Effet |
|---|---|---|
| Point d'accès ouvert | Miraj <version> <édition> - point d'accès MCP sur 127.0.0.1:7008. (suivi de , TLS avec un certificat) | Port ouvert après le port principal |
mcp = OFF mais une variable mcp_* donnée | Note : variables mcp_* ignorées, le point d'accès MCP est désactivé (mcp = OFF ; --mcp ON pour l'ouvrir). | Serveur démarré, port MCP fermé |
| Édition Express | AVERTISSEMENT : le point d'accès MCP demande l'édition Entreprise, Cluster ou Developer ; port MCP non ouvert. | Serveur démarré, port MCP fermé |
mcp_port égal à port | Point d'accès MCP : mcp_port (7007) est aussi le port principal ; choisissez un autre port (--mcp-port) ; serveur arrêté. | Serveur arrêté (code de sortie 2) |
mcp_bind hors de la boucle locale sans certificat | Point d'accès MCP : écoute sur 0.0.0.0 hors de la boucle locale sans TLS refusée ; donnez un certificat (--tls-cert, --tls-key) ou gardez mcp_bind sur 127.0.0.1 ; serveur arrêté. | Serveur arrêté (code 2) |
| Port déjà pris | point d'accès MCP : impossible d'écouter sur 127.0.0.1:7008 : … ; serveur arrêté. | Serveur arrêté (code 2) |
| Valeur hors bornes | Valeur invalide pour mcp_port : 70000 (de 0 à 65535), Valeur invalide pour mcp_max_rows : 0 (de 1 à 1000000)… ; --mcp : « peut-être » inconnu (ON, OFF) | Serveur arrêté (code 2) |
Les règles de port et de TLS sont vérifiées avant l'ouverture des bases : une configuration fausse n'est jamais découverte après un long chargement.
20.3.5 TLS (HTTPS)#
Le point d'accès utilise le même certificat que le port principal (tls_cert et tls_key dans miraj_config.xml, ou --tls-cert et --tls-key, voir chapitre 11) :
- sans certificat, le point d'accès parle HTTP en clair et ne peut écouter que sur la boucle locale ;
- avec un certificat, toutes les connexions MCP sont chiffrées, y compris sur la boucle locale : l'adresse devient
https://hôte:7008/mcpet un client en clair n'obtient aucune réponse.--require-tlset--allow-plain-with-tlsne concernent que le port principal ; - hors de la boucle locale (
mcp_bindpublic), le certificat est obligatoire : sans lui le serveur refuse de démarrer.
miraj-server.exe --root D:\miraj\data --log --mcp ON --mcp-bind 0.0.0.0 ^
--tls-cert D:\miraj\tls\cert.pem --tls-key D:\miraj\tls\key.pemLe client doit faire confiance au certificat : avec un certificat émis par une autorité interne ou auto-signé, déclarez-le au client (pour les clients écrits sur Node.js, comme beaucoup de clients en ligne de commande, la variable d'environnement NODE_EXTRA_CA_CERTS désigne un fichier PEM d'autorités supplémentaires).
20.4 Comment fonctionnent les jetons MCP ?#
Un assistant s'authentifie par un jeton : un secret mjt_… présenté dans l'en-tête HTTP Authorization: Bearer mjt_…. Le jeton appartient à un compte Miraj, dont il prend les privilèges, et porte un niveau d'accès qui les plafonne (20.5). Il se révoque sans toucher au compte.
20.4.1 CREATE MCP TOKEN#
CREATE MCP TOKEN nom [FOR compte]
ACCESS {STRUCTURE | READ | WRITE | ADMIN}
[DATABASES (base1, base2, ...)]
[EXPIRE {NEVER | INTERVAL n DAY}]nom: identifiant, identifiant entre accents graves ou chaîne, de 1 à 64 caractères (1470 au-delà). Il est unique sur le serveur, sans égard à la casse, quel que soit le compte (erreur 9035 s'il est pris).FOR compte:'utilisateur'@'hôte'ouutilisateur, comme dansCREATE USER; sansFOR, le jeton appartient au compte de la session. Un rôle ou un compte inconnu est refusé (1396).ACCESS: niveau du jeton (20.5). Un autre mot est une erreur de syntaxe (1064).DATABASES (…): portée du jeton ; sans cette clause, toutes les bases que le compte peut atteindre. Les noms ne sont pas vérifiés à la création : une base créée plus tard sous ce nom entre dans la portée.EXPIRE INTERVAL n DAY: le jeton expirenjours après sa création (n≥ 1 ;0est une erreur de syntaxe) ;EXPIRE NEVER(défaut) : jamais.- Les clauses qui suivent
ACCESSs'écrivent dans n'importe quel ordre, chacune une fois.
L'instruction rend une ligne avec le secret, affiché cette seule fois :
CREATE MCP TOKEN assistant_ventes FOR 'app'@'localhost'
ACCESS READ DATABASES (ventes, catalogue) EXPIRE INTERVAL 90 DAY;+------------------+------------------------------+-------------------+--------+---------------------+
| name | token | account | access | expires |
+------------------+------------------------------+-------------------+--------+---------------------+
| assistant_ventes | mjt_Xq7mPt3hKc9WbZ2rNf8sLd4v | 'app'@'localhost' | READ | 2026-12-24 10:15:00 |
+------------------+------------------------------+-------------------+--------+---------------------+Le secret est mjt_ suivi de 24 caractères tirés au sort par le générateur du système. Miraj n'en garde que l'empreinte SHA-256, dans le coffre chiffré des comptes : un secret perdu ne se retrouve pas, il faut supprimer le jeton et en créer un autre. expires est en UTC, NULL pour un jeton sans expiration.
20.4.2 SHOW MCP TOKENS#
SHOW MCP TOKENS;+------------------+------+-----------+--------+-------------------+---------------------+---------------------+
| Name | User | Host | Access | Databases | Created | Expires |
+------------------+------+-----------+--------+-------------------+---------------------+---------------------+
| assistant_ventes | app | localhost | READ | ventes,catalogue | 2026-09-25 10:15:00 | 2026-12-24 10:15:00 |
+------------------+------+-----------+--------+-------------------+---------------------+---------------------+Une ligne par jeton, triée par nom ; Databases est NULL sans portée, Expires NULL sans expiration ; dates en UTC. Le secret et son empreinte ne sont jamais montrés. Un compte qui a le privilège CREATE USER voit tous les jetons du serveur, les autres seulement ceux de leur propre compte. Miraj ne tient pas de date de « dernier usage » des jetons.
20.4.3 DROP MCP TOKEN#
DROP MCP TOKEN assistant_ventes;
DROP MCP TOKEN IF EXISTS assistant_ventes; -- avertissement 9036 s'il n'existe pasLa révocation est immédiate : le jeton est revérifié à chaque requête HTTP, la requête suivante d'une session déjà ouverte reçoit 401. Sans IF EXISTS, un jeton inconnu donne l'erreur 9036.
20.4.4 Droits nécessaires#
| Opération | Droit |
|---|---|
| Créer ou supprimer un jeton de son propre compte | Aucun |
| Créer ou supprimer un jeton d'un autre compte | CREATE USER (1227 sinon) ; SYSTEM_USER en plus si ce compte détient SYSTEM_USER |
SHOW MCP TOKENS | Aucun (ses propres jetons) ; CREATE USER pour les voir tous |
Les jetons se gèrent par le port principal (miraj-cli, application, outil d'administration), jamais depuis une session MCP : même avec un jeton ADMIN, CREATE MCP TOKEN et DROP MCP TOKEN y sont refusés (9034). Un jeton limité ne peut donc jamais fabriquer un jeton plus large. SHOW MCP TOKENS n'y est permis qu'au niveau ADMIN.
20.4.5 Jeton et compte#
- Privilèges : le jeton n'en donne aucun ; la session a ceux du compte (et de ses rôles par défaut), réduits par le niveau et la portée du jeton.
- Hôte du compte : l'adresse du client MCP doit être acceptée par l'hôte du compte, comme pour une connexion par mot de passe. Un jeton de
'app'@'localhost'ne s'utilise que depuis la machine du serveur ; pour un assistant sur une autre machine, liez le jeton à un compte dont l'hôte accepte cette machine ('app'@'10.0.0.%', par exemple). - Compte verrouillé (
ALTER USER … ACCOUNT LOCK) : ses jetons sont refusés (3118 dans le journal, 401 pour le client) jusqu'au déverrouillage. DROP USERsupprime les jetons du compte.- Stockage : les jetons sont rangés dans le coffre des comptes ; ils survivent à un redémarrage et, sous Cluster, suivent les comptes.
20.4.6 Erreurs des jetons#
| Code | Cas |
|---|---|
| 9001 | CREATE MCP TOKEN en édition Express |
| 9034 | Gestion de jetons depuis une session MCP ; SHOW MCP TOKENS sous le niveau ADMIN |
| 9035 | Nom de jeton déjà pris sur le serveur |
| 9036 | DROP MCP TOKEN d'un jeton inconnu (avertissement avec IF EXISTS) |
| 1396 | Compte inconnu, ou rôle, dans FOR |
| 1227 | Jeton d'un autre compte sans CREATE USER |
| 1470 | Nom de jeton vide ou de plus de 64 caractères |
| 1064 | Niveau inconnu, EXPIRE INTERVAL 0 DAY |
À la connexion, un secret inconnu, un jeton expiré, un compte qui est un rôle ou dont l'hôte n'accepte pas le client donnent 1045, un compte verrouillé 3118 ; le client MCP reçoit dans tous les cas un 401 sans détail, l'erreur précise n'étant inscrite qu'au journal.
20.5 Quels niveaux d'accès existent ?#
Quatre niveaux, du plus restreint au plus large : STRUCTURE < READ < WRITE < ADMIN.
20.5.1 Outils par niveau#
tools/list ne montre à l'assistant que les outils de son niveau ; appeler un outil au-dessus est refusé (erreur JSON-RPC -32602, 20.11).
| Niveau | Outils |
|---|---|
STRUCTURE | list_databases, list_objects, describe_table, show_create, search_schema |
READ | + read_rows, get_row, count_rows, explain, execute_sql |
WRITE | + insert_rows, update_rows, delete_rows |
ADMIN | + create_database, create_table, drop_object (les seize outils) |
20.5.2 Instructions permises et refusées#
Chaque instruction exécutée par la session (par execute_sql, par un outil, ou à l'intérieur d'une routine ou d'un déclencheur) demande un niveau selon sa nature ; au-dessus du niveau du jeton, elle est refusée par l'erreur 9034 :
| Niveau | Permis | Refusé (9034) |
|---|---|---|
STRUCTURE | SHOW, DESCRIBE, USE, SET de session, transactions, SELECT sans table ou sur information_schema seulement | Toute lecture de données : SELECT sur une table, y compris dans une sous-requête (SET @n = (SELECT …)), EXPLAIN, CALL ; toute écriture |
READ | + SELECT, SELECT … INTO des variables, EXPLAIN, CALL, CHECK TABLE, LOCK TABLES … READ, REFRESH VIEW, LISTEN, UNLISTEN, WAIT FOR CHANGES, SHOW LISTENERS | INSERT, UPDATE, DELETE, REPLACE, LOAD DATA, NOTIFY, tables temporaires, et toute écriture dans le corps d'une routine ou d'un déclencheur |
WRITE | + INSERT, UPDATE, DELETE, REPLACE, LOAD DATA, NOTIFY, LOCK TABLES … WRITE, CREATE / DROP TEMPORARY TABLE | DDL (CREATE, ALTER, DROP, RENAME de bases, tables, vues, routines, déclencheurs, événements), TRUNCATE, REPAIR / OPTIMIZE TABLE, comptes et GRANT / REVOKE, BACKUP / RESTORE, SET GLOBAL, KILL, FLUSH, SELECT … INTO OUTFILE |
ADMIN | Tout ce que le compte permet | Gestion des jetons MCP (toujours) |
Deux réglages de session sont réservés au niveau ADMIN : SET max_statement_time et SET max_execution_time, parce que le point d'accès y fixe le délai mcp_statement_timeout. SET ROLE reste permis à tous les niveaux : les privilèges des rôles activés restent réduits au niveau du jeton.
Le message nomme l'instruction et le niveau :
ERROR 9034 (HY000): INSERT is not allowed at MCP access level READ20.5.3 Un plafond qui s'ajoute aux privilèges#
Le niveau ne donne jamais de privilège : il retire ceux qui le dépassent. Une session MCP ne peut faire que ce que son compte peut faire et ce que son niveau permet. Même root est plafonné par un jeton READ ; un jeton ADMIN d'un compte qui n'a que SELECT sur une table ne lit que cette table.
Concrètement, les privilèges effectifs de la session sont réduits à ceux du niveau :
| Niveau | Privilèges gardés |
|---|---|
STRUCTURE | SHOW VIEW, SHOW DATABASES, REFERENCES |
READ | + SELECT, EXECUTE, LOCK TABLES |
WRITE | + INSERT, UPDATE, DELETE, CREATE TEMPORARY TABLES |
ADMIN | Tous ceux du compte |
Un objet sur lequel le compte a un privilège retiré par le niveau reste visible : un jeton STRUCTURE décrit une table que le compte peut lire, sans pouvoir la lire. Sous ADMIN, les privilèges retirés par le niveau (PROCESS, FILE…) reviennent ; en dessous, SHOW PROCESSLIST ne montre ainsi que les sessions du compte.
20.5.4 Portée DATABASES#
Avec DATABASES (…), les autres bases n'existent plus pour la session, même pour un compte qui a tous les privilèges globaux :
SHOW DATABASES,list_databases,search_schemaetinformation_schemane les montrent pas ;- les désigner est refusé comme si le compte n'y avait aucun droit (1044, 1142),
USE autre_basecompris ; - les privilèges accordés au niveau global (
ON *.*) valent sur chaque base de la portée, sansSHOW DATABASESglobal ni optionGRANTglobale ;CREATE DATABASEd'une base hors portée est refusé (1044).
20.5.5 Routines, déclencheurs et vues#
- Le corps d'une procédure, d'une fonction ou d'un déclencheur est soumis au niveau de la session, instruction par instruction : avec un jeton
READ,CALLd'une procédure qui insère est refusé (9034) au moment de l'INSERT, de même qu'unSELECTqui appelle une fonction qui écrit.PREPAREpasse,EXECUTEest contrôlé sur l'instruction préparée. - Avec un jeton
WRITE, les déclencheurs se déclenchent normalement ; une procédure qui fait du DDL est refusée. - Les droits d'un définisseur (vue, routine
SQL SECURITY DEFINER) ne sont pas réduits : une vue lit ce que son définisseur lui permet, comme pour tout compte. Les instructions de son corps restent soumises au niveau. Tenez-en compte avant d'ouvrir à un jeton une base qui contient des vues ou des routines définies par un compte plus large. - Un refus survient avant la validation implicite d'un DDL : une transaction ouverte n'est pas validée par un
CREATE TABLErefusé.
20.6 Comment connecter un client MCP ?#
Le point d'accès parle le transport MCP « Streamable HTTP » : une seule adresse, http://hôte:port/mcp (ou https://… avec un certificat), et le jeton dans l'en-tête Authorization.
20.6.1 Client MCP générique (fichier JSON)#
La plupart des clients MCP décrivent leurs serveurs dans un fichier JSON. Par exemple, le fichier .mcp.json d'un projet :
{
"mcpServers": {
"miraj": {
"type": "http",
"url": "https://serveur-bd.exemple.local:7008/mcp",
"headers": {
"Authorization": "Bearer mjt_Xq7mPt3hKc9WbZ2rNf8sLd4v"
}
}
}
}Le nom des clés (type, transport, headers…) varie d'un client à l'autre : reportez-vous à sa documentation. Deux constantes : le transport est HTTP (« streamable HTTP »), et l'en-tête Authorization: Bearer <secret> doit accompagner chaque requête. Le secret donne accès à vos données : ne versionnez pas ce fichier avec le secret en clair (préférez, si le client le permet, une variable d'environnement).
20.6.2 Clients qui ne savent lancer qu'un processus (stdio)#
Certains clients ne parlent MCP qu'à un processus qu'ils lancent (transport stdio). Un pont miraj-mcp (stdio vers HTTP, jeton dans une variable d'environnement) est prévu mais pas encore livré. En attendant, utilisez un client qui accepte le transport HTTP, ou un pont stdio vers HTTP tiers qui sache transmettre l'en-tête Authorization (non fourni ni validé par Miraj).
20.7 Référence des outils#
20.7.1 Forme des réponses et des erreurs#
Un appel d'outil (tools/call) rend toujours :
{
"content": [{"type": "text", "text": "…le même JSON que structuredContent, en texte…"}],
"structuredContent": { … },
"isError": false
}structuredContentsuit le schéma de sortie (outputSchema) de l'outil ;content[0].texten est la copie en texte, pour les clients qui ne lisent pas les résultats structurés.- Une erreur SQL ou des arguments refusés ne sont pas des erreurs du protocole : l'appel réussit avec
isError: trueet un champerror, pour que l'assistant lise l'erreur et corrige son appel :
{"results": [], "warnings": [],
"error": {"code": 9034, "sqlstate": "HY000", "message": "INSERT is not allowed at MCP access level READ"}}- Des arguments mal formés (argument inconnu, type faux, valeur manquante, opérateur inconnu…) donnent l'erreur 1210 (
Incorrect arguments to read_rows ('where' is required; …)), avant toute exécution. Un nom de colonne inconnu donne 1054. - Les résultats de lecture ont tous la même forme (
read_rows,get_row,explain,execute_sql) :
{
"results": [
{"columns": [{"name": "id", "type": "int"}, {"name": "nom", "type": "varchar(20)"}],
"rows": [[2, "cahier"]],
"row_count": 1, "truncated": true,
"affected_rows": null, "last_insert_id": null}
],
"warnings": []
}truncated vaut true quand des lignes n'ont pas été rendues (limite de lignes ou de taille) ; warnings liste au plus 64 avertissements {code, message}.
- Garde-fous communs : au plus
mcp_max_rowslignes par résultat, et environ 340 Ko de lignes en JSON par appel (la réponse entière reste sous 1 Mo) ; au-delà,truncated: true. - Visibilité : les outils voient ce que
SHOWmontrerait au compte plafonné. Une base invisible donne 1044 ; une table sur laquelle le compte n'a aucun droit donne 1142, qu'elle existe ou non (son existence n'est pas révélée) ; puis 1049 (base inconnue) et 1146 (table inconnue).information_scheman'est pas servie par les outils structurés (1210) : interrogez-la parexecute_sql. - Les noms de base, de table et de colonne se comparent sans égard à la casse ; les résultats reprennent le nom exact.
20.7.2 list_databases — STRUCTURE#
Liste les bases visibles du jeton (privilèges du compte et portée DATABASES), sans information_schema.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
| (aucun) |
Résultat : {"databases": ["catalogue", "ventes"]}.
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "list_databases", "arguments": {}}}20.7.3 list_objects — STRUCTURE#
Liste les objets d'une base : tables (avec un nombre de lignes estimé), vues, procédures, fonctions, déclencheurs, événements. Un objet sur lequel le compte n'a aucun droit n'est pas listé ; routines et événements demandent un droit sur la base, un déclencheur est visible avec sa table.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
kind | table, view, procedure, function, trigger, event | non | Une seule nature d'objet (1210 pour une autre valeur) |
Résultat : {"database": …, "objects": [ … ]}, chaque objet avec name et kind, plus rows (tables : lignes physiques moins lignes supprimées), table, timing (BEFORE, AFTER) et event (INSERT, UPDATE, DELETE) pour un déclencheur, status pour un événement, comment pour une routine ou un événement.
{"name": "list_objects", "arguments": {"database": "ventes"}}{"database": "ventes", "objects": [
{"name": "article", "kind": "table", "rows": 3},
{"name": "v_stock", "kind": "view"},
{"name": "maj_stock", "kind": "trigger", "table": "ligne", "timing": "AFTER", "event": "INSERT"}]}20.7.4 describe_table — STRUCTURE#
Décrit une table ou une vue sans lire ses données.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
table | chaîne | oui | Table ou vue |
Résultat pour une table : database, table, kind (table), rows (estimé), columns (pour chacune name, type SQL complet tel que decimal(8,2), nullable, default en texte ou null, key PRI/UNI/MUL, extra tel que auto_increment, comment), primary_key, unique et indexes ({name, columns}), vector_index ({name, column, m, distance} ou null), foreign_keys ({name, columns, ref_database, ref_table, ref_columns, on_delete, on_update}), checks ({name, expression, enforced}). Pour une vue : kind view et ses colonnes seulement.
{"name": "describe_table", "arguments": {"database": "ventes", "table": "article"}}{"database": "ventes", "table": "article", "kind": "table", "rows": 3,
"columns": [
{"name": "id", "type": "int", "nullable": false, "default": null, "key": "PRI", "extra": "auto_increment", "comment": ""},
{"name": "prix", "type": "decimal(8,2)", "nullable": true, "default": null, "key": "", "extra": "", "comment": ""}],
"primary_key": ["id"], "unique": [], "indexes": [], "vector_index": null, "foreign_keys": [], "checks": []}20.7.5 show_create — STRUCTURE#
Rend la définition SQL (CREATE …) d'un objet, comme SHOW CREATE.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
name | chaîne | oui | Nom de l'objet |
kind | table, view, procedure, function, trigger, event | non | Sans kind, le nom est cherché comme table, puis comme vue |
Résultat : {"database", "name", "kind", "sql"}.
{"name": "show_create", "arguments": {"database": "ventes", "name": "article"}}{"database": "ventes", "name": "article", "kind": "table", "sql": "CREATE TABLE `article` (…)"}20.7.6 search_schema — STRUCTURE#
Cherche les tables, vues, colonnes, procédures et fonctions dont le nom contient un texte, dans toutes les bases visibles. Recherche sans égard à la casse, texte simple (ni % ni _ jokers). Les routines demandent un droit sur la base, comme dans list_objects.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
pattern | chaîne | oui | Texte cherché dans les noms |
limit | entier, 1 à 1000 | non | Correspondances rendues au plus (100 par défaut) |
Résultat : {"matches": [ … ], "truncated": …}, chaque correspondance avec database et kind (table, view, column, procedure ou function) : table pour une table, une vue ou une colonne (plus column et type pour une colonne), name pour une routine.
{"name": "search_schema", "arguments": {"pattern": "prix"}}{"matches": [{"database": "ventes", "table": "article", "kind": "column", "column": "prix", "type": "decimal(8,2)"}],
"truncated": false}20.7.7 read_rows — READ#
Lit les lignes d'une table ou d'une vue sans écrire de SQL.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
table | chaîne | oui | Table ou vue |
columns | tableau de noms | non | Colonnes rendues (toutes par défaut, ou si le tableau est vide) |
where | objet | non | Condition structurée (20.8) |
order_by | tableau | non | Noms de colonnes, ou {"column": nom, "desc": true} ; par défaut, la clé primaire (pagination stable) |
limit | entier ≥ 1 | non | Lignes rendues au plus : 100 par défaut, ramené à mcp_max_rows |
offset | entier ≥ 0 | non | Lignes sautées d'abord |
Résultat : forme commune des lectures (20.7.1), un seul résultat ; truncated est exact : il vaut true s'il existe au moins une ligne de plus que limit.
{"name": "read_rows", "arguments": {
"database": "ventes", "table": "article",
"columns": ["id", "nom"],
"where": {"column": "prix", "op": ">=", "value": 2},
"order_by": [{"column": "prix", "desc": true}],
"limit": 1}}{"results": [{"columns": [{"name": "id", "type": "int"}, {"name": "nom", "type": "varchar(20)"}],
"rows": [[2, "cahier"]], "row_count": 1, "truncated": true, "affected_rows": null, "last_insert_id": null}],
"warnings": []}Pour paginer, gardez le tri par défaut et augmentez offset de limit tant que truncated vaut true. Un nom de colonne ou d'ordre de tri qui n'est pas un nom exact de colonne ("prix DESC", "id, nom FROM x") est refusé (1054).
20.7.8 get_row — READ#
Lit la ligne d'une table dont la clé primaire vaut key.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
table | chaîne | oui | Table (avec une clé primaire) |
key | objet {colonne: valeur} | oui | Toutes les colonnes de la clé primaire, et elles seules |
Résultat : forme commune des lectures, zéro ou une ligne. Une clé incomplète, une colonne hors de la clé ou une table sans clé primaire (une vue, par exemple) sont refusées (1210).
{"name": "get_row", "arguments": {"database": "ventes", "table": "article", "key": {"id": 3}}}{"results": [{"columns": [ … ], "rows": [[3, "règle", 3.25, -1, null]], "row_count": 1, "truncated": false,
"affected_rows": null, "last_insert_id": null}], "warnings": []}20.7.9 count_rows — READ#
Compte les lignes d'une table ou d'une vue, éventuellement celles qui vérifient une condition.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
table | chaîne | oui | Table ou vue |
where | objet | non | Condition structurée (20.8) |
Résultat : {"count": n, "warnings": []}.
{"name": "count_rows", "arguments": {"database": "ventes", "table": "article",
"where": {"column": "nom", "op": "like", "value": "%e%"}}}{"count": 2, "warnings": []}20.7.10 explain — READ#
Affiche le plan d'exécution d'une instruction SELECT, UPDATE, DELETE ou INSERT … SELECT sans l'exécuter (EXPLAIN).
| Argument | Type | Requis | Rôle |
|---|---|---|---|
sql | chaîne | oui | Une seule instruction |
Résultat : forme commune des lectures, une ligne par accès à une table (1000 lignes au plus). Un texte de plusieurs instructions (SELECT 1; DROP DATABASE ventes) ou une autre instruction est refusé (1210) : un ; ne peut pas faire passer une seconde instruction. Une erreur de syntaxe rend 1064.
{"name": "explain", "arguments": {"sql": "SELECT nom FROM ventes.article WHERE id = 1"}}20.7.11 insert_rows — WRITE#
Insère jusqu'à 1000 lignes, chacune donnée comme {colonne: valeur}.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
table | chaîne | oui | Table |
rows | tableau de 1 à 1000 objets | oui | Lignes ; une colonne omise prend sa valeur par défaut |
on_duplicate | error, ignore, update | non | Ligne dont la clé existe déjà : erreur 1062 (error, défaut), ligne sautée (ignore, comme INSERT IGNORE), ligne existante mise à jour avec les valeurs données hors clé primaire (update, comme ON DUPLICATE KEY UPDATE) |
Les lignes qui donnent les mêmes colonnes sont insérées par une même instruction ; des jeux de colonnes différents donnent plusieurs instructions, exécutées dans une transaction ouverte par l'outil et annulée au premier échec (rien n'est inséré). Si la session a déjà une transaction ouverte, l'outil y travaille sans la valider. Contraintes CHECK (4025), clés étrangères (1452), unicité et déclencheurs s'appliquent.
Résultat : {"affected_rows", "last_insert_id", "statements", "warnings"} ; last_insert_id est la première valeur AUTO_INCREMENT générée par la dernière insertion, null si la table n'a pas de colonne AUTO_INCREMENT ; statements : instructions exécutées. En cas d'échec, error et affected_rows à 0 si la transaction de l'outil a été annulée.
{"name": "insert_rows", "arguments": {"database": "ventes", "table": "article",
"rows": [{"nom": "gomme", "prix": 0.5}, {"nom": "feutre", "prix": 1}]}}{"affected_rows": 2, "last_insert_id": 4, "statements": 1, "warnings": []}20.7.12 update_rows — WRITE#
Modifie les lignes qui vérifient une condition.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
table | chaîne | oui | Table |
set | objet {colonne: valeur} | oui | Nouvelles valeurs (au moins une colonne) |
where | objet | oui, sauf all_rows | Condition structurée (20.8) |
all_rows | booléen | non | true pour modifier toutes les lignes sans condition |
Un where absent ou vide ({}, {"and": []}) est refusé (1210) sans "all_rows": true : un assistant ne modifie pas toute une table par oubli. Résultat : {"affected_rows", "last_insert_id" (null), "statements", "warnings"}.
{"name": "update_rows", "arguments": {"database": "ventes", "table": "article",
"set": {"prix": 9.99}, "where": {"column": "id", "op": "=", "value": 2}}}{"affected_rows": 1, "last_insert_id": null, "statements": 1, "warnings": []}20.7.13 delete_rows — WRITE#
Supprime les lignes qui vérifient une condition ; même règle que update_rows pour where et all_rows.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
table | chaîne | oui | Table |
where | objet | oui, sauf all_rows | Condition structurée (20.8) |
all_rows | booléen | non | true pour supprimer toutes les lignes |
{"name": "delete_rows", "arguments": {"database": "ventes", "table": "article",
"where": {"column": "nom", "op": "=", "value": "gomme"}}}{"affected_rows": 1, "last_insert_id": null, "statements": 1, "warnings": []}20.7.14 create_database — ADMIN#
| Argument | Type | Requis | Rôle |
|---|---|---|---|
name | chaîne, 1 à 64 caractères | oui | Nom de la base |
charset | chaîne | non | Jeu de caractères (utf8mb4) : lettres, chiffres et _ seulement |
collation | chaîne | non | Collation (utf8mb4_general_ci), mêmes caractères |
if_not_exists | booléen | non | Sans erreur si la base existe (avertissement 1007) |
Résultat : {"sql", "warnings"}, sql étant l'instruction exécutée, rendue même en cas d'erreur.
{"name": "create_database", "arguments": {"name": "atelier"}}{"sql": "CREATE DATABASE `atelier`", "warnings": []}20.7.15 create_table — ADMIN#
Crée une table depuis une définition structurée. Miraj écrit le texte SQL canonique (identifiants entre accents graves, chaînes échappées, types pris dans une liste blanche, expressions CHECK relues puis réécrites) avant de l'exécuter : rien n'est recopié tel quel.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
database | chaîne | oui | Base |
table | chaîne | oui | Nom de la table |
columns | tableau d'objets (au moins un) | oui | Colonnes, voir ci-dessous |
primary_key | tableau de noms | non | Clé primaire |
unique | tableau de tableaux de noms | non | Contraintes UNIQUE, une par liste |
indexes | tableau de {name?, columns} | non | Index secondaires |
foreign_keys | tableau d'objets | non | {name?, columns, ref_database?, ref_table, ref_columns, on_delete?, on_update?} ; ref_database par défaut : la base de la table ; actions RESTRICT, CASCADE, SET NULL, SET DEFAULT, NO ACTION |
checks | tableau de chaînes | non | Expressions CHECK ("prix >= 0"), sans sous-requête ni paramètre |
vector_index | objet | non | Index vectoriel HNSW, voir ci-dessous |
partitioning | objet | non | Partitionnement, voir ci-dessous |
if_not_exists | booléen | non | Sans erreur si la table existe |
Chaque colonne :
| Clé | Type | Requis | Rôle |
|---|---|---|---|
name | chaîne | oui | Nom (1 à 64 caractères) |
type | chaîne | oui | Type SQL : TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT (taille facultative), BIT, FLOAT, DOUBLE, DECIMAL(p,s), BOOLEAN, CHAR(n), VARCHAR(n), BINARY(n), VARBINARY(n), TEXT et BLOB (et leurs variantes TINY, MEDIUM, LONG), DATE, TIME, DATETIME, TIMESTAMP (précision facultative), YEAR, JSON, UUID, VECTOR(n), ENUM, SET ; UNSIGNED pour les entiers, DECIMAL, FLOAT et DOUBLE. Les types spatiaux sont refusés |
values | tableau de chaînes | pour ENUM et SET | Membres |
nullable | booléen | non | true par défaut ; false : NOT NULL |
default | valeur JSON | non | Valeur littérale (null, booléen, nombre, chaîne, {"hex": …}) ; "CURRENT_TIMESTAMP" ou "CURRENT_TIMESTAMP(n)" pour une colonne de date et d'heure. Toute autre expression ("NOW()") devient une chaîne |
auto_increment | booléen | non | AUTO_INCREMENT |
comment | chaîne | non | Commentaire |
Une clé inconnue, un type hors liste, un nombre de paramètres faux (VARCHAR sans taille) sont refusés (1210) avant toute exécution. Résultat : {"sql", "warnings"}.
{"name": "create_table", "arguments": {
"database": "atelier", "table": "commande",
"columns": [
{"name": "id", "type": "int", "nullable": false, "auto_increment": true},
{"name": "client", "type": "int", "nullable": false},
{"name": "montant", "type": "decimal(10,2)", "default": 0},
{"name": "cree", "type": "datetime", "default": "CURRENT_TIMESTAMP"}],
"primary_key": ["id"],
"indexes": [{"name": "i_montant", "columns": ["montant"]}],
"foreign_keys": [{"columns": ["client"], "ref_table": "client", "ref_columns": ["id"], "on_delete": "CASCADE"}],
"checks": ["montant >= 0"]}}{"sql": "CREATE TABLE `atelier`.`commande` (`id` INT NOT NULL AUTO_INCREMENT, `client` INT NOT NULL, `montant` DECIMAL(10,2) DEFAULT 0, `cree` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `i_montant` (`montant`), FOREIGN KEY (`client`) REFERENCES `atelier`.`client` (`id`) ON DELETE CASCADE, CHECK (…))",
"warnings": []}Index vectoriel (vector_index) :
| Clé | Type | Requis | Rôle |
|---|---|---|---|
column | chaîne | oui | Colonne VECTOR(n) déclarée NOT NULL |
name | chaîne | non | Nom (par défaut, celui de la colonne) |
m | entier | non | Liens par nœud (3 à 200, 1912 sinon) |
distance | euclidean, cosine, dot | non | Métrique servie |
comment | chaîne | non | Commentaire |
Partitionnement (partitioning, clause PARTITION BY) :
| Clé | Type | Requis | Rôle |
|---|---|---|---|
method | chaîne | oui | RANGE, RANGE COLUMNS, LIST, LIST COLUMNS, HASH, LINEAR HASH, KEY, LINEAR KEY |
expression | chaîne | pour RANGE, LIST, HASH | Expression, relue puis réécrite comme un CHECK |
columns | tableau de noms | pour … COLUMNS | Colonnes ; facultatif pour KEY (clé primaire) |
algorithm | entier | non | Algorithme de KEY |
count | entier, 1 à 8192 | non | PARTITIONS n (HASH, KEY) |
partitions | tableau d'objets | pour RANGE, LIST | {name, less_than?, values_in?, comment?, subpartitions?} : less_than est un tableau de littéraux ({"maxvalue": true} pour MAXVALUE) ou la chaîne "MAXVALUE" ; values_in, un tableau de littéraux (un tableau par valeur pour LIST COLUMNS sur plusieurs colonnes) |
subpartition | objet | non | {method, expression?, columns?, algorithm?, count?} avec HASH, LINEAR HASH, KEY ou LINEAR KEY |
Les règles du moteur (clé primaire contenant les colonnes de partitionnement, bornes croissantes…) s'appliquent : leurs erreurs sont rendues telles quelles. Options de table, colonnes générées et ON UPDATE ne sont pas pris en charge par cet outil : utilisez execute_sql.
20.7.16 drop_object — ADMIN#
Supprime une table, une vue ou une base entière avec ses données. Irréversible.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
kind | table, view, database | oui | Nature de l'objet |
name | chaîne | oui | Nom de l'objet |
database | chaîne | pour une table ou une vue | Base de l'objet |
confirm | chaîne | oui | Doit répéter name exactement (même casse) ; sinon rien n'est supprimé (1210) |
Résultat : {"sql", "warnings"}.
{"name": "drop_object", "arguments": {"database": "atelier", "name": "commande", "kind": "table", "confirm": "commande"}}{"sql": "DROP TABLE `atelier`.`commande`", "warnings": []}Routines, déclencheurs, événements et index se suppriment par execute_sql.
20.7.17 execute_sql — READ et au-delà#
Exécute un script SQL (une ou plusieurs instructions séparées par ;) et rend chaque résultat. Le niveau du jeton filtre ce qui passe (20.5.2) : avec un jeton READ, l'outil ne fait que lire.
| Argument | Type | Requis | Rôle |
|---|---|---|---|
sql | chaîne | oui | Script SQL |
params | tableau | non | Valeurs des marqueurs ?, dans l'ordre (20.9) |
max_rows | entier ≥ 1 | non | Lignes gardées par résultat : 100 par défaut, ramené à mcp_max_rows |
Résultat : forme commune des lectures, un résultat par instruction ; une instruction qui écrit donne un résultat sans colonnes avec affected_rows et last_insert_id. En cas d'erreur, les résultats déjà obtenus sont gardés, suivis d'error ; les instructions suivantes du script ne sont pas exécutées.
{"name": "execute_sql", "arguments": {
"sql": "SELECT id, nom, prix, gros, image FROM ventes.article WHERE id <= ? ORDER BY id",
"params": [2]}}{"results": [{"columns": [{"name": "id", "type": "int"}, {"name": "nom", "type": "varchar(20)"},
{"name": "prix", "type": "decimal(8,2)"}, {"name": "gros", "type": "bigint"}, {"name": "image", "type": "varbinary(4)"}],
"rows": [[1, "stylo", 1.50, "9007199254740993", "0x00FF"], [2, "cahier", 20.00, 7, null]],
"row_count": 2, "truncated": false, "affected_rows": null, "last_insert_id": null}],
"warnings": []}À savoir :
- la session MCP est persistante : variables de session,
USE, tables temporaires et transactions ouvertes (START TRANSACTIONdans un appel,COMMITdans un autre) se conservent d'un appel à l'autre, jusqu'à la fermeture de la session ; max_rowsborne ce qui est rendu, pas ce qui est calculé : pour une grosse table, écrivez unLIMITdans la requête ;LOAD DATA LOCAL INFILEest refusé (1148) : le client MCP n'a pas de fichier à fournir ;- les annotations de l'outil suivent le niveau : lecture seule en
READ, destructif au-delà.
20.8 Conditions where structurées#
read_rows, count_rows, update_rows et delete_rows prennent leur condition sous forme d'objet JSON, jamais de texte SQL.
20.8.1 Condition simple#
{"column": "prix", "op": ">=", "value": 10}op | SQL équivalent | value |
|---|---|---|
=, != (ou <>) | =, <> | une valeur ; avec null, IS NULL / IS NOT NULL |
<, <=, >, >= | comparaison | une valeur |
like, not_like | LIKE, NOT LIKE | une chaîne, jokers SQL % et _ |
in, not_in | IN (…), NOT IN (…) | un tableau de 1 à 10 000 valeurs |
between | BETWEEN a AND b | un tableau de deux bornes |
is_null, is_not_null | IS NULL, IS NOT NULL | aucune (value refusée) |
column doit être un nom de colonne de la table (1054 sinon, sans égard à la casse) ; les valeurs deviennent des littéraux : "' OR 1=1 --" reste une chaîne comparée telle quelle.
20.8.2 Groupes#
{"and": [ condition, … ]} {"or": [ condition, … ]} {"not": condition}Chaque groupe est la seule clé de son objet. Exemple :
{"and": [
{"column": "prix", "op": ">=", "value": 10},
{"or": [{"column": "nom", "op": "=", "value": "a"}, {"column": "nom", "op": "=", "value": "b"}]},
{"not": {"column": "id", "op": "=", "value": 3}}
]}équivaut à prix >= 10 AND (nom = 'a' OR nom = 'b') AND NOT (id = 3).
Règles :
{}et{"and": []}ne posent aucune condition (et comptent comme unwhereabsent pourupdate_rowsetdelete_rows) ; unorvide est refusé plutôt que lu comme « toutes les lignes » ;- profondeur d'imbrication 32 au plus, 1000 conditions simples au plus ;
- une clé inconnue (
"sql": "1=1") ou une condition en texte ("where": "id = 1") est refusée (1210).
20.9 Valeurs JSON#
20.9.1 Valeurs rendues#
| Type SQL | JSON rendu | Exemple |
|---|---|---|
| Entiers | nombre ; chaîne au-delà de 2^53 (9 007 199 254 740 992) en valeur absolue, pour les lecteurs JSON qui lisent les nombres en flottants | 7, "9007199254740993" |
DECIMAL | nombre écrit exactement, sans passer par un flottant | 1.50 |
FLOAT, DOUBLE | nombre ; null pour une valeur infinie ou NaN | 2.5 |
Chaînes, JSON | chaîne | "stylo" |
| Dates et heures | chaîne au format SQL | "2026-09-25 10:15:00" |
Binaire (BINARY, VARBINARY, BLOB) | chaîne hexadécimale 0x… en majuscules | "0x00FF" |
NULL | null | null |
Un décimal est exact dans le texte de la réponse (1.50) ; un lecteur JSON qui le convertit en flottant peut l'afficher 1.5. Le type SQL de chaque colonne figure dans columns[].type.
20.9.2 Valeurs données#
Dans where, rows, set, key et default :
| JSON | Valeur SQL |
|---|---|
null, true, false | NULL, TRUE, FALSE |
| entier | entier ; au-delà de la plage des entiers signés 64 bits, passé comme une chaîne de chiffres convertie par la colonne |
nombre à virgule sans exposant (12.50, au plus 18 décimales) | décimal exact |
nombre avec exposant (1e300) | flottant |
| chaîne | chaîne ; les dates s'écrivent "AAAA-MM-JJ" ou "AAAA-MM-JJ hh:mm:ss" |
{"hex": "00ff"} (préfixe 0x accepté) ou {"base64": "AP8="} | octets |
objet ou tableau, pour une colonne JSON d'une table | texte JSON sérialisé |
Un tableau ou un autre objet est refusé pour une colonne non JSON (1210).
Dans les params d'execute_sql, mêmes valeurs, à deux différences près : un nombre à virgule y est passé comme flottant (donnez une chaîne, "12.50", pour un décimal exact), et objets ou tableaux ne sont acceptés que sous la forme {"hex"} / {"base64"}.
20.10 Ressources MCP#
En plus des outils, le point d'accès publie des ressources JSON (application/json), filtrées par les mêmes privilèges que les outils STRUCTURE :
| URI | Contenu |
|---|---|
miraj://{base} | Objets de la base, comme list_objects |
miraj://{base}/{table} | Description de la table ou de la vue, comme describe_table, avec en plus create : sa définition CREATE |
resources/listrend une ressource par base visible (miraj://ventes, nomventes, titreDatabase ventes) ;resources/templates/listrend les deux modèlesmiraj://{database}etmiraj://{database}/{table};resources/readrend{"contents": [{"uri", "mimeType", "text"}]},textétant le JSON du contenu ;- les noms hors lettres, chiffres,
-,.,_et~s'encodent en pourcentage (miraj://ma%20base/a%2Fb) ; - une URI mal formée donne l'erreur JSON-RPC -32602, une ressource introuvable ou refusée -32002 ;
- l'abonnement aux changements des ressources n'est pas proposé.
20.11 Protocole (pour les intégrateurs)#
Cette section décrit les échanges pour qui écrit son propre client ; un client MCP ordinaire s'en charge seul.
20.11.1 Transport#
- Streamable HTTP sans flux SSE : chaque requête JSON-RPC est un
POST /mcpdont la réponse est un corpsapplication/json; HTTP/1.1 (connexions persistantes) ou HTTP/1.0. - Un seul message par requête : les lots (tableau JSON) sont refusés (-32600).
- Corps de 16 Mo au plus ;
Transfer-Encoding: chunkedetExpect: 100-continueacceptés. DELETE /mcpferme la session ;GET /mcp(flux du serveur) n'est pas proposé (405) ; tout autre chemin : 404.- Versions du protocole reconnues :
2025-11-25,2025-06-18,2025-03-26.initializeretient celle du client si elle est reconnue, sinon propose la plus récente.
20.11.2 En-têtes#
| En-tête | Sens | Règle |
|---|---|---|
Authorization: Bearer mjt_… | requête | Exigé sur chaque requête, DELETE compris (401 sinon) |
Content-Type: application/json | requête | S'il est présent, doit être application/json (415 sinon) |
Mcp-Session-Id | réponse d'initialize, puis chaque requête | 32 caractères hexadécimaux tirés au sort ; exigé après initialize (400 sans lui, 404 inconnu ou expiré) ; une session n'accepte que le jeton qui l'a ouverte (401) |
MCP-Protocol-Version | requête | Facultatif ; s'il est présent, doit être une version reconnue (400 sinon) |
Origin | requête | S'il est présent, doit être une origine de la boucle locale (http(s)://localhost, 127.0.0.1 ou [::1], port quelconque), sinon 403 : protection contre les pages web qui viseraient le serveur depuis un navigateur |
WWW-Authenticate: Bearer realm="miraj" | réponse 401 | |
Cache-Control: no-store | toute réponse |
20.11.3 Déroulement d'une session#
POST /mcp initialize → 200, en-tête Mcp-Session-Id, capacités, instructions
POST /mcp notifications/initialized → 202 (sans corps)
POST /mcp tools/list → 200, outils du niveau du jeton
POST /mcp tools/call → 200, résultat de l'outil (isError éventuel)
DELETE /mcp → 204, session fermée, transaction ouverte annuléeAvec curl (syntaxe d'un shell POSIX) :
curl -i http://127.0.0.1:7008/mcp \
-H "Authorization: Bearer mjt_Xq7mPt3hKc9WbZ2rNf8sLd4v" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"essai","version":"1"}}}'Le résultat d'initialize annonce serverInfo (name miraj, title Miraj, version), les capacités tools et resources (sans listChanged ni abonnement) et un texte instructions qui présente à l'assistant le jeton, le compte, le niveau, la portée et la limite de lignes.
Méthodes servies : initialize, ping, tools/list, tools/call, resources/list, resources/templates/list, resources/read, et les notifications notifications/initialized et notifications/cancelled (qui interrompt l'appel en cours, comme KILL QUERY). Toute autre notification, et toute réponse du client, reçoit 202 et est ignorée ; toute autre méthode (prompts/list, par exemple) donne -32601. Une session traite une requête à la fois : des requêtes simultanées sur la même session s'exécutent l'une après l'autre.
20.11.4 Codes HTTP#
| Code | Cas |
|---|---|
| 200 | Réponse JSON-RPC (succès, erreur JSON-RPC ou résultat d'outil avec isError) |
| 202 | Notification ou réponse du client acquittée, sans corps |
| 204 | DELETE : session fermée |
| 400 | JSON illisible (-32700), message invalide (-32600), Mcp-Session-Id manquant, MCP-Protocol-Version inconnue, requête HTTP mal formée |
| 401 | Jeton absent, inconnu, expiré, révoqué, compte verrouillé ou hôte refusé ; session ouverte par un autre jeton |
| 403 | Origin étrangère à la boucle locale ; adresse bloquée après trop d'échecs (message de l'erreur 1129 ; avec TLS, la connexion d'une adresse bloquée est seulement fermée, sans réponse) |
| 404 | Session inconnue ou expirée (« Session not found or expired: initialize a new session ») ; chemin autre que /mcp |
| 405 | Méthode autre que POST et DELETE (en-tête Allow: POST, DELETE) |
| 413 | Corps de plus de 16 Mo |
| 415 | Content-Type autre que application/json |
| 431 | En-têtes trop longs (32 Ko) ou trop nombreux (64) |
| 501 | Transfer-Encoding autre que chunked |
| 503 | max_connections atteint à l'ouverture d'une session (message de l'erreur 1040) |
| 505 | Version HTTP autre que 1.0 et 1.1 |
Une connexion doit présenter sa première requête dans les 10 secondes (poignée de main TLS comprise), et une requête commencée doit arriver entière dans les 30 secondes.
20.11.5 Erreurs JSON-RPC#
| Code | Cas |
|---|---|
| -32700 | Corps qui n'est pas du JSON |
| -32600 | Message invalide : lot, jsonrpc différent de "2.0", id qui n'est ni chaîne ni nombre, method absente ; initialize répété dans une session ; Mcp-Session-Id manquant ; version de protocole inconnue ; Content-Type refusé |
| -32601 | Méthode inconnue |
| -32602 | tools/call sans nom, outil inconnu ou au-dessus du niveau du jeton, arguments qui n'est pas un objet ; resources/read sans uri ou avec une URI mal formée |
| -32002 | Ressource introuvable ou refusée |
| -32000 | Erreur du serveur accompagnant un code HTTP 401, 403, 404 ou 503 |
Les erreurs SQL et les arguments refusés ne sont pas des erreurs JSON-RPC : ils arrivent dans un résultat isError: true (20.7.1).
20.12 Comment sécuriser le serveur MCP ?#
- Le niveau le plus bas utile. Un jeton
STRUCTUREsuffit pour qu'un assistant écrive des requêtes d'après le schéma ;READpour qu'il les vérifie sur les données ;WRITEetADMINseulement sur une base de travail ou de test. - Un compte dédié. Le niveau plafonne, il ne remplace pas les privilèges : créez un compte propre à l'assistant, avec les seuls
GRANTnécessaires, plutôt qu'un jeton deroot. - Une portée par base (
DATABASES (…)) et une expiration (EXPIRE INTERVAL n DAY) : un secret oublié dans un fichier de configuration cesse d'ouvrir quoi que ce soit. - Un jeton par usage (par assistant, par poste, par projet), pour révoquer l'un sans toucher aux autres :
DROP MCP TOKENagit dès la requête suivante. - Protéger le secret. Il n'est montré qu'une fois ; rangez-le dans le gestionnaire de secrets ou la configuration du client, jamais dans un dépôt de sources.
- Boucle locale par défaut. Laissez
mcp_bindsur127.0.0.1quand l'assistant tourne sur la machine du serveur ; hors de la boucle locale, TLS est obligatoire, et l'hôte du compte doit accepter la machine de l'assistant. - Origine contrôlée. Une requête qui porte un en-tête
Originétranger à la boucle locale est refusée (403) : une page web ne peut pas utiliser le navigateur d'un poste pour atteindre le point d'accès. - Journal sans secret. Avec
--log, le journal inscrit le nom du jeton, le compte, l'outil, la durée, le nombre de lignes et le code d'erreur, jamais le secret ni les données ; le SQL d'execute_sqly est tronqué à 200 caractères, mots de passe masqués. - Délais.
mcp_statement_timeoutborne chaque appel,mcp_idle_timeoutferme les sessions oubliées et annule leur transaction ; seul un jetonADMINpeut relevermax_statement_timepour sa session, jamais le délai d'appel du point d'accès. - Échecs d'authentification. Un secret faux compte comme un échec d'authentification pour l'adresse du client, exactement comme sur le port principal (11.5.2) : réponse retardée, puis adresse bloquée après
max_connect_errorséchecs consécutifs, même avec un bon jeton (le registre des échecs est celui du port principal). Un en-têteAuthorizationabsent n'est pas compté. Pour débloquer :
FLUSH HOSTS; -- privilège RELOAD- Suppression confirmée.
drop_objectexige de répéter le nom ;update_rowsetdelete_rowsexigent une condition saufall_rows: true. Ces garde-fous évitent les erreurs, ils ne remplacent pas un niveau adapté : un jetonADMINpeut tout supprimer parexecute_sql.
20.13 Supervision#
20.13.1 Sessions#
Chaque session MCP est une session Miraj ordinaire :
- elle apparaît dans
SHOW PROCESSLIST(etinformation_schema.PROCESSLIST) sous l'utilisateur du compte du jeton, la colonneHostvalantmcp:<nom du jeton>@<hôte du client>, par exemplemcp:assistant@localhost;USER()etCURRENT_USER()n'en montrent rien et un compte sansPROCESSvoit ces sessions comme les autres sessions de son utilisateur ; - elle prend une place de
max_connections(503 au-delà) ; KILL QUERY idinterrompt l'appel en cours (1317 pour l'assistant),KILL CONNECTION idferme la session (404 à la requête suivante, transaction annulée) ;idest le numéro de session deSHOW PROCESSLIST, aussi écrit entre crochets dans le journal.
Le fil de ménage du point d'accès ferme une session restée sans requête mcp_idle_timeout secondes : sa transaction ouverte est annulée et le client reçoit 404 à sa requête suivante (il doit rouvrir une session par initialize). Il abandonne aussi l'appel qui dépasse mcp_statement_timeout : l'instruction en cours s'arrête par l'erreur 1969 (limite de chaque instruction) ou 1317 (appel entier interrompu), et la session reste utilisable.
20.13.2 Journal#
Avec --log, le point d'accès écrit :
[12] session MCP ouverte depuis 127.0.0.1, jeton lecteur, compte app@localhost, niveau READ, protocole 2025-06-18, client mon-client
[12] MCP tools/call read_rows, jeton lecteur, compte app@localhost : 2.4 ms, 1 ligne(s) -- read_rows ventes.article
[12] MCP tools/call execute_sql, jeton lecteur, compte app@localhost : 0.8 ms, 0 ligne(s), erreur 9034 -- INSERT INTO ventes.article (nom) VALUES ('gomme')
[12] session MCP fermée par le client, jeton lecteur
[13] session MCP fermée (inactive ou arrêtée), jeton lecteur
MCP ! 1045 (28000): Access denied for user 'mcp'@'10.0.0.5' (using password: YES) -- depuis 10.0.0.5
SÉCURITÉ : adresse 10.0.0.5 bloquée après 100 échecs d'authentification consécutifs (max_connect_errors) ; FLUSH HOSTS la débloqueLes appels réussis et les ouvertures de session s'affichent sur la console ; les appels en erreur, les refus d'authentification et les événements SÉCURITÉ sont en plus inscrits dans <root>\miraj\server.log.
20.14 Limites connues#
- Le pont stdio
miraj-mcp, pour les clients qui ne savent lancer qu'un processus, n'est pas encore livré (20.6.2). - Pas de flux SSE ni de notification du serveur (
GET /mcp: 405), pas de lot JSON-RPC, pas de prompts MCP, pas d'abonnement aux ressources. create_tablene crée ni options de table, ni colonnes générées, niON UPDATE;drop_objectne supprime que tables, vues et bases : le reste passe parexecute_sql(jetonADMIN).search_schemane cherche pas dans les noms de déclencheurs et d'événements.get_rowdemande une clé primaire : pas de lecture par clé sur une vue ; les colonnesJSONd'une vue ne reçoivent pas d'objet ou de tableau en valeur.- Les descriptions d'outils, les instructions de session et les messages du protocole sont en anglais.
- Pas de date de dernier usage des jetons.
- Édition Cluster : la réplication des jetons avec les comptes et le rejeu sur les secondaires du DDL créé par
create_tablerestent à valider par un essai en cluster.
20.15 Voir aussi#
- 10. Comptes et privilèges : comptes, rôles,
GRANT. - 11. Administration du serveur :
miraj_config.xml, TLS,max_connections,max_connect_errors, journal. - 15. Codes d'erreur : 9034, 9035, 9036.
- 19. Recherche vectorielle.