Mirajv1.0
FR

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 ?#

ÉditionPoint d'accès MCP
EntrepriseDisponible
ClusterDisponible
DeveloperDisponible (avec les limites propres à l'édition : un cœur, 20 Go, 24 heures)
ExpressNon 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)ArgumentDéfautValeursRôle
mcp--mcp ON|OFFOFFON, OFF (aussi true, false, 1, 0)Ouvre ou non le point d'accès
mcp_port--mcp-port <port>70080 à 65535Port 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.1adresse IP ou nomAdresse d'écoute, indépendante de bind ; hors de la boucle locale, TLS obligatoire (20.3.5)
mcp_idle_timeout--mcp-idle-timeout <s>3001 à 31 536 000 secondesUne 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>300 à 31 536 000 secondes, fraction permise (2.5) ; 0 : sans limiteDurée maximale d'un appel d'outil (erreurs 1969 ou 1317 au-delà, voir 20.13)
mcp_max_rows--mcp-max-rows <n>50001 à 1 000 000Plafond 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 1000

Dé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#

SituationMessageEffet
Point d'accès ouvertMiraj <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éeNote : 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 ExpressAVERTISSEMENT : 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 à portPoint 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 certificatPoint 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à prispoint d'accès MCP : impossible d'écouter sur 127.0.0.1:7008 : … ; serveur arrêté.Serveur arrêté (code 2)
Valeur hors bornesValeur 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/mcp et un client en clair n'obtient aucune réponse. --require-tls et --allow-plain-with-tls ne concernent que le port principal ;
  • hors de la boucle locale (mcp_bind public), 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.pem

Le 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' ou utilisateur, comme dans CREATE USER ; sans FOR, 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 expire n jours après sa création (n ≥ 1 ; 0 est une erreur de syntaxe) ; EXPIRE NEVER (défaut) : jamais.
  • Les clauses qui suivent ACCESS s'é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 pas

La 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érationDroit
Créer ou supprimer un jeton de son propre compteAucun
Créer ou supprimer un jeton d'un autre compteCREATE USER (1227 sinon) ; SYSTEM_USER en plus si ce compte détient SYSTEM_USER
SHOW MCP TOKENSAucun (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 USER supprime 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#

CodeCas
9001CREATE MCP TOKEN en édition Express
9034Gestion de jetons depuis une session MCP ; SHOW MCP TOKENS sous le niveau ADMIN
9035Nom de jeton déjà pris sur le serveur
9036DROP MCP TOKEN d'un jeton inconnu (avertissement avec IF EXISTS)
1396Compte inconnu, ou rôle, dans FOR
1227Jeton d'un autre compte sans CREATE USER
1470Nom de jeton vide ou de plus de 64 caractères
1064Niveau 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).

NiveauOutils
STRUCTURElist_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 :

NiveauPermisRefusé (9034)
STRUCTURESHOW, DESCRIBE, USE, SET de session, transactions, SELECT sans table ou sur information_schema seulementToute 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 LISTENERSINSERT, 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 TABLEDDL (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
ADMINTout ce que le compte permetGestion 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 READ

20.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 :

NiveauPrivilèges gardés
STRUCTURESHOW VIEW, SHOW DATABASES, REFERENCES
READ+ SELECT, EXECUTE, LOCK TABLES
WRITE+ INSERT, UPDATE, DELETE, CREATE TEMPORARY TABLES
ADMINTous 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_schema et information_schema ne les montrent pas ;
  • les désigner est refusé comme si le compte n'y avait aucun droit (1044, 1142), USE autre_base compris ;
  • les privilèges accordés au niveau global (ON *.*) valent sur chaque base de la portée, sans SHOW DATABASES global ni option GRANT globale ; CREATE DATABASE d'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, CALL d'une procédure qui insère est refusé (9034) au moment de l'INSERT, de même qu'un SELECT qui appelle une fonction qui écrit. PREPARE passe, EXECUTE est 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 TABLE refusé.

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
}
  • structuredContent suit le schéma de sortie (outputSchema) de l'outil ; content[0].text en 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: true et un champ error, 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_rows lignes 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 SHOW montrerait 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_schema n'est pas servie par les outils structurés (1210) : interrogez-la par execute_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.

ArgumentTypeRequisRô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.

ArgumentTypeRequisRôle
databasechaîneouiBase
kindtable, view, procedure, function, trigger, eventnonUne 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.

ArgumentTypeRequisRôle
databasechaîneouiBase
tablechaîneouiTable 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.

ArgumentTypeRequisRôle
databasechaîneouiBase
namechaîneouiNom de l'objet
kindtable, view, procedure, function, trigger, eventnonSans 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.

ArgumentTypeRequisRôle
patternchaîneouiTexte cherché dans les noms
limitentier, 1 à 1000nonCorrespondances 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.

ArgumentTypeRequisRôle
databasechaîneouiBase
tablechaîneouiTable ou vue
columnstableau de nomsnonColonnes rendues (toutes par défaut, ou si le tableau est vide)
whereobjetnonCondition structurée (20.8)
order_bytableaunonNoms de colonnes, ou {"column": nom, "desc": true} ; par défaut, la clé primaire (pagination stable)
limitentier ≥ 1nonLignes rendues au plus : 100 par défaut, ramené à mcp_max_rows
offsetentier ≥ 0nonLignes 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.

ArgumentTypeRequisRôle
databasechaîneouiBase
tablechaîneouiTable (avec une clé primaire)
keyobjet {colonne: valeur}ouiToutes 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.

ArgumentTypeRequisRôle
databasechaîneouiBase
tablechaîneouiTable ou vue
whereobjetnonCondition 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).

ArgumentTypeRequisRôle
sqlchaîneouiUne 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}.

ArgumentTypeRequisRôle
databasechaîneouiBase
tablechaîneouiTable
rowstableau de 1 à 1000 objetsouiLignes ; une colonne omise prend sa valeur par défaut
on_duplicateerror, ignore, updatenonLigne 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.

ArgumentTypeRequisRôle
databasechaîneouiBase
tablechaîneouiTable
setobjet {colonne: valeur}ouiNouvelles valeurs (au moins une colonne)
whereobjetoui, sauf all_rowsCondition structurée (20.8)
all_rowsbooléennontrue 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.

ArgumentTypeRequisRôle
databasechaîneouiBase
tablechaîneouiTable
whereobjetoui, sauf all_rowsCondition structurée (20.8)
all_rowsbooléennontrue 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#

ArgumentTypeRequisRôle
namechaîne, 1 à 64 caractèresouiNom de la base
charsetchaînenonJeu de caractères (utf8mb4) : lettres, chiffres et _ seulement
collationchaînenonCollation (utf8mb4_general_ci), mêmes caractères
if_not_existsbooléennonSans 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.

ArgumentTypeRequisRôle
databasechaîneouiBase
tablechaîneouiNom de la table
columnstableau d'objets (au moins un)ouiColonnes, voir ci-dessous
primary_keytableau de nomsnonClé primaire
uniquetableau de tableaux de nomsnonContraintes UNIQUE, une par liste
indexestableau de {name?, columns}nonIndex secondaires
foreign_keystableau d'objetsnon{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
checkstableau de chaînesnonExpressions CHECK ("prix >= 0"), sans sous-requête ni paramètre
vector_indexobjetnonIndex vectoriel HNSW, voir ci-dessous
partitioningobjetnonPartitionnement, voir ci-dessous
if_not_existsbooléennonSans erreur si la table existe

Chaque colonne :

CléTypeRequisRôle
namechaîneouiNom (1 à 64 caractères)
typechaîneouiType 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
valuestableau de chaînespour ENUM et SETMembres
nullablebooléennontrue par défaut ; false : NOT NULL
defaultvaleur JSONnonValeur 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_incrementbooléennonAUTO_INCREMENT
commentchaînenonCommentaire

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éTypeRequisRôle
columnchaîneouiColonne VECTOR(n) déclarée NOT NULL
namechaînenonNom (par défaut, celui de la colonne)
mentiernonLiens par nœud (3 à 200, 1912 sinon)
distanceeuclidean, cosine, dotnonMétrique servie
commentchaînenonCommentaire

Partitionnement (partitioning, clause PARTITION BY) :

CléTypeRequisRôle
methodchaîneouiRANGE, RANGE COLUMNS, LIST, LIST COLUMNS, HASH, LINEAR HASH, KEY, LINEAR KEY
expressionchaînepour RANGE, LIST, HASHExpression, relue puis réécrite comme un CHECK
columnstableau de nomspour … COLUMNSColonnes ; facultatif pour KEY (clé primaire)
algorithmentiernonAlgorithme de KEY
countentier, 1 à 8192nonPARTITIONS n (HASH, KEY)
partitionstableau d'objetspour 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)
subpartitionobjetnon{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.

ArgumentTypeRequisRôle
kindtable, view, databaseouiNature de l'objet
namechaîneouiNom de l'objet
databasechaînepour une table ou une vueBase de l'objet
confirmchaîneouiDoit 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.

ArgumentTypeRequisRôle
sqlchaîneouiScript SQL
paramstableaunonValeurs des marqueurs ?, dans l'ordre (20.9)
max_rowsentier ≥ 1nonLignes 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 TRANSACTION dans un appel, COMMIT dans un autre) se conservent d'un appel à l'autre, jusqu'à la fermeture de la session ;
  • max_rows borne ce qui est rendu, pas ce qui est calculé : pour une grosse table, écrivez un LIMIT dans la requête ;
  • LOAD DATA LOCAL INFILE est 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}
opSQL équivalentvalue
=, != (ou <>)=, <>une valeur ; avec null, IS NULL / IS NOT NULL
<, <=, >, >=comparaisonune valeur
like, not_likeLIKE, NOT LIKEune chaîne, jokers SQL % et _
in, not_inIN (…), NOT IN (…)un tableau de 1 à 10 000 valeurs
betweenBETWEEN a AND bun tableau de deux bornes
is_null, is_not_nullIS NULL, IS NOT NULLaucune (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 un where absent pour update_rows et delete_rows) ; un or vide 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 SQLJSON renduExemple
Entiersnombre ; 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 flottants7, "9007199254740993"
DECIMALnombre écrit exactement, sans passer par un flottant1.50
FLOAT, DOUBLEnombre ; null pour une valeur infinie ou NaN2.5
Chaînes, JSONchaîne"stylo"
Dates et heureschaîne au format SQL"2026-09-25 10:15:00"
Binaire (BINARY, VARBINARY, BLOB)chaîne hexadécimale 0x… en majuscules"0x00FF"
NULLnullnull

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 :

JSONValeur SQL
null, true, falseNULL, TRUE, FALSE
entierentier ; 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înechaî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 tabletexte 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 :

URIContenu
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/list rend une ressource par base visible (miraj://ventes, nom ventes, titre Database ventes) ;
  • resources/templates/list rend les deux modèles miraj://{database} et miraj://{database}/{table} ;
  • resources/read rend {"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 /mcp dont la réponse est un corps application/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: chunked et Expect: 100-continue acceptés.
  • DELETE /mcp ferme 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. initialize retient celle du client si elle est reconnue, sinon propose la plus récente.

20.11.2 En-têtes#

En-têteSensRègle
Authorization: Bearer mjt_…requêteExigé sur chaque requête, DELETE compris (401 sinon)
Content-Type: application/jsonrequêteS'il est présent, doit être application/json (415 sinon)
Mcp-Session-Idréponse d'initialize, puis chaque requête32 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-VersionrequêteFacultatif ; s'il est présent, doit être une version reconnue (400 sinon)
OriginrequêteS'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-storetoute 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ée

Avec 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#

CodeCas
200Réponse JSON-RPC (succès, erreur JSON-RPC ou résultat d'outil avec isError)
202Notification ou réponse du client acquittée, sans corps
204DELETE : session fermée
400JSON illisible (-32700), message invalide (-32600), Mcp-Session-Id manquant, MCP-Protocol-Version inconnue, requête HTTP mal formée
401Jeton absent, inconnu, expiré, révoqué, compte verrouillé ou hôte refusé ; session ouverte par un autre jeton
403Origin é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)
404Session inconnue ou expirée (« Session not found or expired: initialize a new session ») ; chemin autre que /mcp
405Méthode autre que POST et DELETE (en-tête Allow: POST, DELETE)
413Corps de plus de 16 Mo
415Content-Type autre que application/json
431En-têtes trop longs (32 Ko) ou trop nombreux (64)
501Transfer-Encoding autre que chunked
503max_connections atteint à l'ouverture d'une session (message de l'erreur 1040)
505Version 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#

CodeCas
-32700Corps qui n'est pas du JSON
-32600Message 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é
-32601Méthode inconnue
-32602tools/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
-32002Ressource introuvable ou refusée
-32000Erreur 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 STRUCTURE suffit pour qu'un assistant écrive des requêtes d'après le schéma ; READ pour qu'il les vérifie sur les données ; WRITE et ADMIN seulement 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 GRANT nécessaires, plutôt qu'un jeton de root.
  • 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 TOKEN agit 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_bind sur 127.0.0.1 quand 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_sql y est tronqué à 200 caractères, mots de passe masqués.
  • Délais. mcp_statement_timeout borne chaque appel, mcp_idle_timeout ferme les sessions oubliées et annule leur transaction ; seul un jeton ADMIN peut relever max_statement_time pour 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ête Authorization absent n'est pas compté. Pour débloquer :
FLUSH HOSTS;   -- privilège RELOAD
  • Suppression confirmée. drop_object exige de répéter le nom ; update_rows et delete_rows exigent une condition sauf all_rows: true. Ces garde-fous évitent les erreurs, ils ne remplacent pas un niveau adapté : un jeton ADMIN peut tout supprimer par execute_sql.

20.13 Supervision#

20.13.1 Sessions#

Chaque session MCP est une session Miraj ordinaire :

  • elle apparaît dans SHOW PROCESSLIST (et information_schema.PROCESSLIST) sous l'utilisateur du compte du jeton, la colonne Host valant mcp:<nom du jeton>@<hôte du client>, par exemple mcp:assistant@localhost ; USER() et CURRENT_USER() n'en montrent rien et un compte sans PROCESS voit ces sessions comme les autres sessions de son utilisateur ;
  • elle prend une place de max_connections (503 au-delà) ;
  • KILL QUERY id interrompt l'appel en cours (1317 pour l'assistant), KILL CONNECTION id ferme la session (404 à la requête suivante, transaction annulée) ; id est le numéro de session de SHOW 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ébloque

Les 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_table ne crée ni options de table, ni colonnes générées, ni ON UPDATE ; drop_object ne supprime que tables, vues et bases : le reste passe par execute_sql (jeton ADMIN).
  • search_schema ne cherche pas dans les noms de déclencheurs et d'événements.
  • get_row demande une clé primaire : pas de lecture par clé sur une vue ; les colonnes JSON d'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_table restent à valider par un essai en cluster.

20.15 Voir aussi#