Understand akko-mcp-trino in five levels

A path that starts with the problem and ends with the proof. Each level fits on one screen, carries one idea and is meant to be played with. Level 1 is an illustration; the others rest on the public sandbox, its real access rules and real answers.

Level 1 · understand in 30 seconds

An agent plugged in with an account that sees everything

Switch between the two setups and watch what Trino receives.

Carol, an analyst, asks for the customer list

With a broad service account

This setup is an illustration, without akko-mcp-trino.

Carolcarol_analyst
The agentaccount: mcp-svc
Trino seesmcp-svc

10 customers, e-mails in clear. Trino does not know that Carol is asking. Her rules (European customers only, masked e-mails) no longer apply. All that is left is an instruction in the prompt.

With akko-mcp-trino

jwt mode, the default

Carolcarol_analyst
The agentCarol’s token
Trino seescarol_analyst

5 European customers, masked e-mails. Trino recognises Carol and applies her rules, exactly as in her SQL console or her dashboards.

Remember. The rights are already written in Trino. The only real work is to get the right identity all the way to it.

Level 2 · a small game

Who sees what?

Same question, same MCP client, same server. Guess first, then check.

How many of the 10 customers does Carol see?

Pick an answer.

execute_querySELECT full_name, email, country FROM demo.crm.customers ORDER BY id

Alice is an administrator: she sees every row and every column in clear.

What Trino returns to alice_admin
full_nameemailcountry
Lena Vogellena.vogel@example.comDE
Hugo Martinhugo.martin@example.comFR
Sofia Ruizsofia.ruiz@example.orgES
Noah Bennettnoah.bennett@example.comUS
Giulia Contigiulia.conti@example.orgIT
Kenji Satokenji.sato@example.comJP
Emma de Vriesemma.devries@example.orgNL
Lucas Almeidalucas.almeida@example.comBR
Olivia Clarkeolivia.clarke@example.orgGB
Chloe Tremblaychloe.tremblay@example.comCA

Carol is an analyst: a row filter country IN ('DE', 'ES', 'FR', 'IT', 'NL') and a column mask on email.

What Trino returns to carol_analyst
full_nameemailcountry
Lena Vogel***@example.comDE
Hugo Martin***@example.comFR
Sofia Ruiz***@example.orgES
Noah Bennett (row removed by the row filter)***@example.comUS
Giulia Conti***@example.orgIT
Kenji Sato (row removed by the row filter)***@example.comJP
Emma de Vries***@example.orgNL
Lucas Almeida (row removed by the row filter)***@example.comBR
Olivia Clarke (row removed by the row filter)***@example.orgGB
Chloe Tremblay (row removed by the row filter)***@example.comCA

Dave is a viewer: the customers table is refused to him (and hidden from listings), as is the accounts’ balance column.

Access Denied: Cannot select from columns [country, full_name, id, email] in table or view demo.crm.customers

Remember. These answers come from what Trino lets each user see, with the sandbox’s OPA policy. The MCP server copied no rule: it only passed on each person’s identity.

Level 3 · the agent, step by step

How the agent finds the right columns

Follow a real Mistral trial on 0.7.0, tool call by tool call. The calls are the trial’s, word for word. The tool answers were replayed on the same server version.

  1. 1 · Search

    search_columns

    “Which customer segment holds the largest total account balance?”

    The agent does not know the tables. It searches with four words from the question, in one tool call over every catalog it may see.

    search_columns {"text": "customer segment account balance"}
  2. 2 · Word by word

    the server answers

    No column holds all four words, so the server searches each word

    Each result says which words found it, and "partial": true warns the agent. The marked columns are the ones its answer will use.

    • crm.accounts.balanceaccountbalanceused
    • crm.customers.segmentcustomersegmentused
    • crm.accounts.customer_idcustomerused
    • crm.transactions.account_idaccount
    • crm.transactions.amountaccount
    Excerpt of the server’s answer
    "partial": true,
    "hint": "No table or column holds every word together, so these are partial matches, each found by the words it lists: pick the right one, or search again with a single word from a column or table name, then call describe_table on the table."
  3. 3 · A wrong join

    execute_query

    The agent joins on c.customer_id, a column customers does not have

    It writes the query straight away, without reading the customers table.

    execute_query {"sql": "SELECT c.segment, SUM(a.balance) AS total_balance FROM demo.crm.customers c JOIN demo.crm.accounts a ON c.customer_id = a.customer_id GROUP BY c.segment ORDER BY total_balance DESC LIMIT 1", "max_rows": 1}
  4. 4 · The error guides

    the server answers

    The error says which table to read

    Since 0.7.0, an error carries a kind and, for a column that cannot be found, a hint that names the table to read.

    {"error": "TrinoUserError(type=USER_ERROR, name=COLUMN_NOT_FOUND, message=\"line 1:105: Column 'c.customer_id' cannot be resolved\", query_id=20261101_211014_00019_85v63)", "kind": "column", "identifier": "c.customer_id", "hint": "Read the columns of demo.crm.customers with describe_table, then write the query again with a column it has."}
  5. 5 · Read the table

    describe_table

    customers has an id column, not customer_id

    The agent follows the hint and reads the columns of customers.

    describe_table {"catalog": "demo", "schema": "crm", "table": "customers"}
    {"columns": ["Column", "Type", "Extra", "Comment"], "rows": [["id", "integer", "", ""], ["full_name", "varchar", "", ""], ["email", "varchar", "", ""], ["country", "char(2)", "", ""], ["segment", "varchar", "", "Retail or business customer"]], "truncated": false}
  6. 6 · Fix and answer

    execute_query

    The business segment, with a total balance of 17,620.75

    The agent runs the query again with c.id = a.customer_id. Trino computes it over the customers Carol may see: the 17,620.75 is in EUR, Sofia Ruiz’s two accounts, because Carol’s row filter removes the business customers outside Europe. The agent cites the columns it used. The same path comes back in 4 of the 5 trials of this question.

    execute_query {"sql": "SELECT c.segment, SUM(a.balance) AS total_balance FROM demo.crm.customers c JOIN demo.crm.accounts a ON c.id = a.customer_id GROUP BY c.segment ORDER BY total_balance DESC LIMIT 1", "max_rows": 1}
    {"columns": ["segment", "total_balance"], "rows": [["business", "17620.75"]], "row_count": 1, "truncated": false}
    The customer segment that holds the largest total account balance is the business segment, with a total balance of 17620.75.
    
    The columns relied on are:
    - demo.crm.customers.segment
    - demo.crm.accounts.balance
    - demo.crm.customers.id
    - demo.crm.accounts.customer_id
1 / 6

Level 4 · your situation

Which identity mode is yours?

Answer the questions in order and the right mode lights up. The four diagrams share one grid, so you can see what changes.

Can your identity provider exchange tokens (RFC 8693, as Keycloak 26 does)?
Can Trino allow a service account to act on behalf of users?
Does the server run on one person’s workstation (Claude Desktop, Cursor), with Trino’s OAuth2 login?

Answer the first question.

From the safest to the simplest: jwt if your identity provider exchanges tokens, impersonation if Trino allows a service account to act on behalf of users, oauth2 on one person’s workstation when Trino has its OAuth2 login, and service otherwise.

jwtdefault

Trino sees the user

MCP hostserverTrinoIdPthe user’s tokenexchanged token

The safest. The server never passes on the token it receives: it obtains another one, issued for Trino.

impersonation

Trino sees the user

MCP hostserverTrinoverified tokenserver’s account,on behalf of the user

Without token exchange. Trino allows this one account to act on behalf of others. The name always comes from the verified token.

oauth2workstation

Trino sees the signed-in person

browserlocal serverTrinosign-in to Trinono name sentstdio only

On the workstation. The browser login of a Trino set up for OAuth2, with no identity provider to set up on the server side.

servicenamed on purpose

Trino sees one account, for everyone

person Aperson BserverTrinomcp-reader

The same rights for everyone, read-only. The server announces it with a warning at start.

Level 5 · the proofs

Measured with real agents, and yours to try

The same five business questions, each with a trap column, asked to Mistral and to Claude.

Right answersHigher is better
0%50%100%62.570Mistral93.393.3Claude
Tool calls per questionLower is better
05105.83.0Mistral8.54.9Claude
0.6.10.7.0

Mistral Codestral answered 40 times per version, and Claude Sonnet 5.5 answered 15 times. Mistral’s tool calls come from the five-trial run. The reports are public in akko-sandbox.

Mistral’s gap stays within its run-to-run swing. Fewer tool calls, but about 41% more input tokens per question, because the tool descriptions are longer.

# Try it in five minutes: Keycloak, Trino, OPA and the server, with the three users of level 2
$ git clone https://github.com/AKKO-p/akko-sandbox.git
$ cd akko-sandbox
$ docker compose up -d --wait
$ ./demo.sh

To ask your own questions from Claude Desktop or Cursor, follow Connect Claude Desktop or Cursor.

Going further

Follow one question end to end in How it works, then read the guarantees and the proofs in the overview. The open source core covers installation and configuration, and the Versions page lists every release.