Le cœur open source

Le cœur de akko-mcp-trino, sous licence Apache 2.0, donne à un agent IA un accès MCP en lecture seule à Trino, sous une identité configurée ou sous celle de chaque utilisateur.

Le cœur de akko-mcp-trino est un serveur MCP pour Trino, sous licence Apache 2.0, en version 0.5.3. Il donne à un agent IA six outils pour explorer un Trino et y lire des données, avec une garde de lecture seule décidée sur l’arbre SQL, des bornes à chaque appel et une ligne d’audit par appel. Chaque requête s’exécute soit sous l’identité que l’administrateur configure, soit sous celle de l’utilisateur pour qui l’agent travaille, dont Trino vérifie lui-même le jeton. Dans les deux cas, Trino et son moteur de politique décident de ce que l’agent peut lire.

Le cœur, et ce que livrent AKKO et ARCHE

Cœur open source Dans AKKO et ARCHE
Identité dans Trino une identité configurée par l’administrateur, ou celle de chaque utilisateur quand Trino fait confiance au même fournisseur d’identité les mêmes, et d’autres façons de porter l’identité de l’utilisateur
Lecture seule sur l’arbre SQL oui oui
Bornes de lignes et de temps oui oui
Traces une ligne d’audit par appel, avec l’appelant un audit relié au journal du moteur
Outils six outils d’exploration et de lecture les mêmes, et des outils supplémentaires
Contexte métier du catalogue non oui, sans jamais décrire ce que la personne ne voit pas
Gestion des agents et des accès non oui
Exploitation en entreprise à votre charge installée et suivie avec la plateforme

Le cœur convient à un agent qui lit sous un compte dont vos règles bornent les droits, comme à des agents qui lisent chacun sous les droits de la personne, sur un Trino qui vérifie les jetons de votre fournisseur d’identité. La présentation décrit le serveur complet.

Installer

pip install akko-mcp-trino        # ou : pipx install akko-mcp-trino
uvx akko-mcp-trino --version      # sans installation, comme le font la plupart des hôtes MCP

Vous pouvez aussi l’installer depuis la source.

git clone https://github.com/AKKO-p/akko-mcp-trino.git
cd akko-mcp-trino
python -m venv .venv && source .venv/bin/activate
pip install -e .

Il faut Python 3.12 ou plus récent. Le paquet installe un module Python (akko_mcp_trino) et une commande (akko-mcp-trino) ; python -m akko_mcp_trino lance le même serveur.

En conteneur

L’image est publiée sur Docker Hub, pour linux/amd64 et linux/arm64. Elle tourne sous un utilisateur non root et porte une sonde de santé. Elle se configure avec les mêmes variables d’environnement, sur les ports 3000 (MCP) et 3001 (santé).

docker run --rm akkoai/akko-mcp-trino:0.5 akko-mcp-trino --version

docker run -d --name akko-mcp-trino -p 3000:3000 -p 3001:3001 \
  -e TRINO_HOST=trino.example.internal -e TRINO_PORT=8443 -e TRINO_HTTP_SCHEME=https \
  -e TRINO_CATALOG=iceberg -e TRINO_IDENTITY_MODE=jwt \
  -e MCP_AUTH_ENABLED=true -e MCP_AUTH_REQUIRED=true \
  -e MCP_JWKS_URL=https://idp.example.com/realms/data/protocol/openid-connect/certs \
  -e MCP_OIDC_ISSUER=https://idp.example.com/realms/data \
  -e MCP_OIDC_AUDIENCE=data-platform \
  akkoai/akko-mcp-trino:0.5

L’étiquette 0.5 suit la dernière version de la branche 0.5 ; 0.5.3 désigne une version précise.

Le code, les tests et les exemples sont sur GitHub ; le journal des versions suit chaque changement, et une vulnérabilité se signale selon la politique de sécurité.

Démarrer

Le serveur lit toute sa configuration dans l’environnement. Pointez-le sur votre Trino et sur votre fournisseur d’identité, puis lancez-le. Cet exemple utilise le mode par défaut, service, où chaque requête s’exécute sous TRINO_USER.

export TRINO_HOST=trino.example.internal
export TRINO_PORT=8443
export TRINO_HTTP_SCHEME=https
export TRINO_CATALOG=iceberg
export TRINO_USER=agent_lecture          # l'identité sous laquelle le serveur lit
export TRINO_PASSWORD=...                # authentification Basic, https seulement

export MCP_AUTH_ENABLED=true
export MCP_AUTH_REQUIRED=true            # refuse une requête sans identité vérifiée
export MCP_JWKS_URL=https://idp.example.com/realms/data/protocol/openid-connect/certs
export MCP_OIDC_ISSUER=https://idp.example.com/realms/data
export MCP_OIDC_AUDIENCE=data-platform

akko-mcp-trino --check                   # la configuration effective, sans aucun secret
akko-mcp-trino                           # sert en streamable-http sur le port 3000

--check affiche la configuration effective sans jamais imprimer un secret, et sort avec le code 2 si le serveur refuserait de démarrer. Au démarrage, le serveur écrit une ligne comme celle-ci.

INFO:__main__:serving transport=streamable-http port=3000 auth=True strict=True extensions=none

Trois points permettent ensuite de suivre le serveur, sur le port de santé.

curl -s localhost:3001/health    # {"status":"ok"}     ne touche jamais Trino
curl -s localhost:3001/ready     # {"status":"ready"}  Trino répond
curl -s localhost:3001/metrics   # métriques Prometheus

/health ne touche jamais Trino, pour qu’une sonde de vie ne tue pas le serveur parce que Trino est lent. /ready interroge Trino selon le mode d’identité : un SELECT 1 borné sous TRINO_USER en mode service, le point public /v1/info de Trino en mode jwt. /metrics expose mcp_trino_queries_total, mcp_trino_query_errors_total et mcp_trino_query_duration_seconds.

L’identité dans Trino

Une requête pas à pas : token vérifié par le MCP server, SQL read-only vérifié, token revérifié par Trino, décision d'OPA, lignes rendues ; et les deux refus (401, Access Denied).

Le serveur vérifie d’abord le jeton de l’appelant (signature contre le JWKS du fournisseur, émetteur, audience, expiration). En mode strict (MCP_AUTH_REQUIRED=true), une requête sans jeton vérifié est refusée avant toute requête. La variable TRINO_IDENTITY_MODE dit ensuite sous quelle identité Trino exécute.

Mode Qui Trino voit Ce que Trino doit avoir Qui décide
service (défaut) TRINO_USER, le même pour tous les appelants un mot de passe, ou rien pour un Trino local les droits de cette seule identité
jwt l’appelant, par son propre jeton vérifié son authentification JWT, qui fait confiance au même fournisseur d’identité les droits de chaque utilisateur

Une identité configurée (service)

Tous les appels de tous les agents qui utilisent ce serveur lisent avec les mêmes droits. Donnez à ce compte un périmètre étroit dans votre moteur de politique (OPA, Ranger ou le contrôle d’accès de Trino) : les catalogues, les schémas et les colonnes dont tous les appelants ont besoin, et rien de plus. Le jeton de l’appelant ne quitte jamais le serveur. Une identité envoyée par l’appelant, dans un en-tête par exemple, ne change rien.

Chaque utilisateur sous ses propres droits (jwt)

Quand Trino fait confiance au même fournisseur d’identité, laissez-le décider pour chaque personne.

export TRINO_IDENTITY_MODE=jwt           # le jeton vérifié de l'appelant part vers Trino
export TRINO_HTTP_SCHEME=https           # obligatoire : un jeton ne circule jamais en http
export MCP_AUTH_ENABLED=true
export MCP_AUTH_REQUIRED=true
# TRINO_USER et TRINO_PASSWORD ne servent pas

Côté Trino, réglez http-server.authentication.type=JWT, http-server.authentication.jwt.key-file sur l’adresse du JWKS du fournisseur, et http-server.authentication.jwt.principal-field sur la même revendication que le serveur lit (preferred_username ou sub). required-issuer et required-audience attachent le jeton à votre fournisseur et à Trino ; l’audience du jeton doit être une audience que Trino accepte.

Le serveur transmet à Trino le jeton qu’il a vérifié, en https seulement, et jamais dans la ligne d’audit. Trino vérifie ce jeton à son tour et exécute la requête sous le nom de la personne : Ranger ou OPA décident pour elle, sans compte de service, sans mot de passe et sans droit d’agir pour quelqu’un d’autre. Une requête sans jeton vérifié est refusée ; elle n’est jamais exécutée sous TRINO_USER. Le serveur refuse de démarrer en mode jwt sur http, et un mode inconnu l’empêche de démarrer.

Ce mode a été prouvé sur un cluster réel : deux comptes posent la même question et obtiennent deux réponses, les emails masqués pour l’un et en clair pour l’autre ; une requête sans jeton et une requête avec un jeton falsifié sont refusées.

Les six outils

Outil Arguments Ce qu’il rend
list_catalogs aucun {"catalogs": [...], "truncated": bool}, les catalogues visibles
list_schemas catalog {"schemas": [...], "truncated": bool}, les schémas de ce catalogue
list_tables catalog, schema {"tables": [...], "truncated": bool}, les tables et vues de ce schéma
describe_table catalog, schema, table, sample_rows (0 à 20) les colonnes, leurs types et commentaires, truncated, et un échantillon gouverné si on le demande
explain_query sql {"plan": "..."}, le plan d’une requête en lecture, sans l’exécuter
execute_query sql, max_rows (facultatif, inférieur au plafond) {"columns": [...], "rows": [...], "row_count": n, "truncated": bool}

Chaque outil passe par Trino sous le mode d’identité, les outils de liste compris : les métadonnées sont des données, et Trino ne liste que ce que cette identité peut voir. Chaque description d’outil est écrite pour un modèle et dit ce que l’outil rend ; celles de describe_table et execute_query précisent qu’une valeur masquée ou une ligne absente est la politique d’accès, pas une erreur à relancer. Les erreurs reviennent à l’agent sous la forme {"error": "..."}, avec le message entier de Trino ; un refus de droit est une erreur ordinaire, pas un plantage.

La lecture seule

execute_query accepte tout le SQL que Trino accepte, à condition que ce soit une lecture. La requête est analysée en arbre syntaxique, et elle est refusée si elle contient plus d’une instruction, ou si une écriture (INSERT, UPDATE, DELETE, MERGE, CREATE, DROP, ALTER, GRANT, CALL, SET) apparaît n’importe où dans l’arbre, CTE, sous-requêtes et instruction expliquée par un EXPLAIN compris. EXPLAIN ANALYZE est refusé, puisqu’il exécute ce qu’il explique.

Les fonctions de table (TABLE(...)) sont refusées : un TABLE(pg.system.query(query => '...')) se lit comme un simple SELECT mais envoie du SQL brut à la source sous le compte du connecteur, hors de la politique du moteur. Le catalogue system est refusé aussi. Chaque identifiant est validé ([A-Za-z_][A-Za-z0-9_-]*) avant d’entrer dans le SQL. Un nom sans catalogue se lit dans le catalogue de session (TRINO_CATALOG) : donnez-lui un catalogue de données.

La garde est une seconde ligne. La première reste le moteur : donnez au compte du serveur, ou aux utilisateurs, des droits de lecture seulement, et refusez system dans votre politique. Le blog raconte comment cette garde a pris sa forme.

Les bornes

Borne Variable Effet
Plafond de lignes TRINO_MAX_ROWS une ligne de plus est lue pour dire si la réponse est coupée (truncated), et le reste de la requête est annulé dans Trino
Budget d’une requête TRINO_QUERY_TIMEOUT_SECONDS une requête plus longue est annulée dans Trino, et l’agent en est informé
Budget d’un appel MCP_CALL_TIMEOUT_SECONDS passé ce délai, aucune requête ne démarre, celle en cours est annulée, et l’agent reçoit une erreur plutôt qu’une réponse partielle

Le budget est tenu par le serveur, pas posé en propriété de session, pour qu’un moteur de politique qui interdit les propriétés de session ne refuse pas toutes les requêtes.

Le journal

Chaque réponse porte un en-tête X-Request-Id : celui de la passerelle quand il est simple, sinon un identifiant généré. Chaque appel d’outil écrit une ligne JSON dans le journal mcp.audit, indexée par cet identifiant.

{"request_id":"edge-42","tool":"execute_query","subject":"alice","ok":false,"error":"USER_ERROR:PERMISSION_DENIED","duration_ms":84}

Avec la configuration de journal par défaut, la ligne sort précédée de INFO:mcp.audit: ; un collecteur qui attend du JSON pur retire ce préfixe. La ligne dit qui a appelé quel outil, avec quel résultat et en combien de temps. Elle n’a aucun champ qui pourrait contenir le jeton ou le texte SQL. Une erreur y figure sous forme de code, jamais de message, parce qu’un message de Trino peut citer une valeur lue dans une ligne (Cannot cast '<valeur>' to INT). L’agent, lui, lit le message entier. Chaque refus porte un en-tête X-Reason ; en mode strict, une requête sans identité vérifiée reçoit 401 unauthenticated.

Brancher un hôte MCP

À distance, l’hôte se connecte à l’adresse du serveur (/mcp en streamable-http, le transport par défaut ; /sse pour un hôte plus ancien) et envoie le jeton d’accès de l’utilisateur.

{
  "mcpServers": {
    "trino": {
      "url": "https://mcp.example.internal/mcp",
      "headers": {"Authorization": "Bearer <le jeton d'accès de l'utilisateur>"}
    }
  }
}

En local, Claude Desktop, Cursor ou Mistral Vibe lancent eux-mêmes le serveur en stdio. Il n’y a pas de requête HTTP : l’appelant vient de l’environnement, par le jeton d’accès de l’utilisateur dans MCP_USER_TOKEN, vérifié comme un en-tête. En mode strict, le serveur refuse de démarrer sans lui.

{
  "mcpServers": {
    "trino": {
      "command": "uvx",
      "args": ["akko-mcp-trino"],
      "env": {
        "MCP_TRANSPORT": "stdio",
        "TRINO_HOST": "trino.example.internal", "TRINO_PORT": "8443", "TRINO_HTTP_SCHEME": "https",
        "TRINO_CATALOG": "iceberg", "TRINO_IDENTITY_MODE": "jwt",
        "MCP_AUTH_ENABLED": "true", "MCP_AUTH_REQUIRED": "true",
        "MCP_JWKS_URL": "https://idp.example.com/realms/data/protocol/openid-connect/certs",
        "MCP_OIDC_ISSUER": "https://idp.example.com/realms/data",
        "MCP_OIDC_AUDIENCE": "data-platform",
        "MCP_USER_TOKEN": "<le jeton d'accès de l'utilisateur>"
      }
    }
  }
}

En mode service, remplacez TRINO_IDENTITY_MODE par TRINO_USER et TRINO_PASSWORD. Filtrez l’en-tête Host avec MCP_ALLOWED_HOSTS quand le serveur est joignable par un navigateur.

Étendre sans modifier

Un paquet Python peut ajouter des outils ou des comportements autour du cœur, sans le modifier. Il s’enregistre sous un groupe de points d’entrée, et l’exploitant le nomme au démarrage.

[project.entry-points."akko_mcp_trino.tools"]
mes-outils = "mon_paquet.outils:register"
export MCP_TOOL_PLUGINS=mes-outils

Rien n’est chargé qui ne soit nommé, et un nom inconnu empêche le démarrage. Un outil ajouté passe par le même client, donc sous le même mode d’identité, avec la même garde de lecture seule et la même ligne de journal. Une extension plus large (MCP_EXTENSIONS, groupe akko_mcp_trino.extensions) peut ajouter des modes d’identité, des gardes à l’entrée, des routes ou des champs de journal, avec une règle : elle peut refuser, jamais élargir.

Configuration

Toute la configuration se lit dans l’environnement ; rien n’est codé en dur.

Trino

Variable Rôle Défaut
TRINO_HOST, TRINO_PORT le coordinateur localhost, 8080
TRINO_HTTP_SCHEME http ou https ; un mot de passe ou un jeton ne part jamais en http http
TRINO_VERIFY vérification TLS : true, false, ou le chemin de l’autorité de Trino true
TRINO_IDENTITY_MODE service (chaque requête sous TRINO_USER) ou jwt (chaque requête sous l’appelant, https seulement) ; un mode inconnu empêche le démarrage service
TRINO_USER l’identité sous laquelle le serveur lit en mode service trino
TRINO_PASSWORD son mot de passe, envoyé en authentification Basic, refusé en http aucun
TRINO_CATALOG le catalogue de session, où se lit un nom sans catalogue ; donnez-lui un catalogue de données system
TRINO_READ_ONLY refuser les écritures dans execute_query true
TRINO_SOURCE la source que Trino enregistre pour chaque requête le nom du serveur
TRINO_REQUEST_TIMEOUT_SECONDS le délai d’une requête HTTP vers Trino 30
TRINO_MAX_ROWS, TRINO_QUERY_TIMEOUT_SECONDS, MCP_CALL_TIMEOUT_SECONDS les bornes ci-dessus 100, 0, 0

Identité

Variable Rôle Défaut
MCP_AUTH_ENABLED vérifier les jetons des appelants false
MCP_AUTH_REQUIRED refuser une requête sans identité vérifiée false
MCP_JWKS_URL le JWKS du fournisseur ; obligatoire quand la vérification est active aucun
MCP_OIDC_ISSUER, MCP_OIDC_AUDIENCE l’émetteur et l’audience exigés aucun
MCP_JWT_LEEWAY_SECONDS la tolérance sur exp, nbf et iat, pour l’écart d’horloge 30
MCP_ALLOWED_HOSTS le filtre de l’en-tête Host, entrées séparées par des virgules ; vide, tout est servi aucun

Service

Variable Rôle Défaut
MCP_TRANSPORT streamable-http, sse ou stdio streamable-http
MCP_PORT, MCP_HEALTH_PORT les ports d’écoute 3000, 3001
MCP_SERVER_NAME le nom annoncé aux hôtes trino-mcp
MCP_USER_TOKEN en stdio seulement, le jeton d’accès de l’utilisateur aucun
MCP_TOOL_PLUGINS, MCP_EXTENSIONS les greffons et extensions à charger, par nom aucun

Compatibilité

Pris en charge
Python 3.12, 3.13
Trino 351 et suivants ; en mode jwt, https avec l’authentification JWT
Moteurs de politique OPA, Ranger, contrôle d’accès fichier de Trino
Fournisseurs d’identité tout fournisseur OIDC qui publie un JWKS (Keycloak, Entra ID, Okta, Dex…)
MCP transports streamable-http, sse et stdio, par le SDK officiel
Modèles tous ; le serveur ne parle jamais à un modèle

Aller plus loin

Pour d’autres façons de porter l’identité de l’utilisateur, un audit relié au journal du moteur, le contexte métier du catalogue et la gestion des agents et des accès, voyez le serveur livré dans AKKO et ARCHE. Pour une question, écrivez-nous. Pour signaler un problème du cœur, ouvrez une issue.