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.
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
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.
| full_name | country | |
|---|---|---|
| Lena Vogel | lena.vogel@example.com | DE |
| Hugo Martin | hugo.martin@example.com | FR |
| Sofia Ruiz | sofia.ruiz@example.org | ES |
| Noah Bennett | noah.bennett@example.com | US |
| Giulia Conti | giulia.conti@example.org | IT |
| Kenji Sato | kenji.sato@example.com | JP |
| Emma de Vries | emma.devries@example.org | NL |
| Lucas Almeida | lucas.almeida@example.com | BR |
| Olivia Clarke | olivia.clarke@example.org | GB |
| Chloe Tremblay | chloe.tremblay@example.com | CA |
Carol is an analyst: a row filter country IN ('DE', 'ES', 'FR', 'IT', 'NL') and a column mask on email.
| full_name | country | |
|---|---|---|
| Lena Vogel | ***@example.com | DE |
| Hugo Martin | ***@example.com | FR |
| Sofia Ruiz | ***@example.org | ES |
| Noah Bennett (row removed by the row filter) | ***@example.com | US |
| Giulia Conti | ***@example.org | IT |
| Kenji Sato (row removed by the row filter) | ***@example.com | JP |
| Emma de Vries | ***@example.org | NL |
| Lucas Almeida (row removed by the row filter) | ***@example.com | BR |
| Olivia Clarke (row removed by the row filter) | ***@example.org | GB |
| Chloe Tremblay (row removed by the row filter) | ***@example.com | CA |
Dave is a viewer: the customers table is refused to him (and hidden from listings), as is the accounts’ balance column.
search_columns {"text": "email", "catalog": "demo"} no match, the search is complete
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 · 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 · Word by word
the server answersNo column holds all four words, so the server searches each word
Each result says which words found it, and
"partial": truewarns 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 · A wrong join
execute_queryThe 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 · The error guides
the server answersThe error says which table to read
Since 0.7.0, an error carries a
kindand, for a column that cannot be found, ahintthat 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 · Read the table
describe_tablecustomers 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 · Fix and answer
execute_queryThe 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
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.
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.
jwtdefaultTrino sees the user
The safest. The server never passes on the token it receives: it obtains another one, issued for Trino.
impersonationTrino sees the user
Without token exchange. Trino allows this one account to act on behalf of others. The name always comes from the verified token.
oauth2workstationTrino sees the signed-in person
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 purposeTrino sees one account, for everyone
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.
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.