Metadata-Version: 2.4
Name: akko-mcp-trino
Version: 0.1.0
Summary: Governed MCP server for Trino: every tool call carries the identity of the user the agent acts for, and the engine decides.
Author-email: Abderrahmen Dridi <contact@akko-ai.com>
Maintainer-email: AKKO <contact@akko-ai.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/AKKO-p/akko-mcp-trino
Project-URL: Documentation, https://github.com/AKKO-p/akko-mcp-trino#readme
Project-URL: Repository, https://github.com/AKKO-p/akko-mcp-trino
Project-URL: Issues, https://github.com/AKKO-p/akko-mcp-trino/issues
Project-URL: Changelog, https://github.com/AKKO-p/akko-mcp-trino/blob/main/CHANGELOG.md
Project-URL: AKKO, https://akko-ai.com
Keywords: mcp,model-context-protocol,trino,lakehouse,data-governance,ai-agents,oidc,jwt,opa,ranger,sql,llm,agent-gateway
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Security
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Typing :: Typed
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp[cli]==1.8.1
Requires-Dist: trino==0.337.0
Requires-Dist: httpx==0.28.0
Requires-Dist: starlette==0.46.2
Requires-Dist: uvicorn==0.34.2
Requires-Dist: sqlglot==25.34.0
Requires-Dist: prometheus-client==0.21.1
Requires-Dist: PyJWT[crypto]==2.10.1
Provides-Extra: test
Requires-Dist: pytest==8.3.4; extra == "test"
Requires-Dist: pytest-cov==6.0.0; extra == "test"
Provides-Extra: dev
Requires-Dist: pytest==8.3.4; extra == "dev"
Requires-Dist: pytest-cov==6.0.0; extra == "dev"
Requires-Dist: ruff==0.9.4; extra == "dev"
Requires-Dist: build==1.2.2; extra == "dev"
Requires-Dist: twine==6.1.0; extra == "dev"
Dynamic: license-file

<p align="center">
  <img src="https://img.shields.io/github/actions/workflow/status/AKKO-p/akko-mcp-trino/ci.yml?branch=main&label=ci" alt="CI">
  <img src="https://img.shields.io/badge/tests-173%20passed-success" alt="Tests">
  <img src="https://img.shields.io/badge/coverage-100%25-brightgreen" alt="Coverage">
  <img src="https://img.shields.io/badge/version-0.1.0-blue" alt="Version">
  <img src="https://img.shields.io/badge/python-3.12%20%7C%203.13-blue" alt="Python">
  <img src="https://img.shields.io/badge/trino-%E2%89%A5%20351%20%C2%B7%20tested%20483-blue" alt="Trino">
  <img src="https://img.shields.io/badge/MCP-1.8%20%C2%B7%20sse%20%7C%20streamable--http-blue" alt="MCP">
  <img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="License">
  <img src="https://img.shields.io/badge/code%20style-ruff-000000" alt="Ruff">
  <img src="https://img.shields.io/badge/status-beta-yellow" alt="Status">
</p>

# akko-mcp-trino

**Give your AI agents access to Trino without giving them your data.**

akko-mcp-trino is a governed [MCP](https://modelcontextprotocol.io) server for
Trino. Every tool call an agent makes carries the identity of the person it
acts for; what that person may read is decided inside Trino, by the policy
engine you already run (OPA, Ranger, or Trino's own access control). The server
never reads on the user's behalf with a service account, and it never decides
access on its own.

## Why

Most MCP servers for SQL engines connect with one technical account. Every
agent, every user, every prompt then reads with the same broad rights, and the
only thing between a prompt injection and your customer table is the model's
good will.

This server takes the opposite stance. The agent brings the user's token. The
token becomes `X-Trino-User`. Trino applies that user's catalog scope, row
filters and column masks, exactly as it does for a BI tool or a notebook.

Same question, same server, two users:

```
$ python examples/agent.py "Give me three customer e-mails with their country"

as alice_admin     marie.martin0@gmail.com, FR   thomas.devries1@outlook.com, DE   léa.dubois2@proton.me, ES
as carol_analyst   ***@gmail.com, FR             ***@outlook.com, FR              ***@proton.me, FR
```

That run is real: a Mistral model driving the tools, on a Trino behind
Keycloak and OPA. The model did not know carol was restricted; it did not need
to.

## Highlights

- **Identity, end to end.** Verified JWT (JWKS, issuer, audience, expiry) → `X-Trino-User`. No impersonation without a verified identity.
- **Read-only by construction.** SQL is parsed into an AST and refused if a write appears anywhere in the tree, CTEs and subqueries included.
- **Two principals.** `X-Agent-Key` names the calling product (Cursor, Claude, your own agent) for quotas and audit; it is never a user.
- **Standard discovery.** RFC 9728 metadata and `WWW-Authenticate` on every `401`, so hosts know where to log in.
- **Quotas, revocation, audit.** Per-user and per-agent limits, optional RFC 7662 introspection, one JSON audit line per call keyed by `X-Request-Id` — never the token.
- **Any OIDC provider, any MCP host, any model.** Keycloak, Entra ID, Okta… Cursor, Claude Desktop, VS Code, the Python SDK… Mistral, or any OpenAI-compatible model through the example agent.
- **Small and proven.** 1 300 lines, 173 tests at 100 % coverage, a product-neutral guard in CI, and three live proofs (functional, adversarial, agent-driven) on a real cluster.

## Contents

1. [How it works](#how-it-works)
2. [Compatibility](#compatibility)
3. [Prerequisites](#prerequisites)
4. [Install and run](#install-and-run)
5. [Connect an MCP host](#connect-an-mcp-host)
6. [Use it from an agent](#use-it-from-an-agent)
7. [The tools](#the-tools)
8. [What happens on a request](#what-happens-on-a-request)
9. [Configuration](#configuration)
10. [Operations](#operations)
11. [Development](#development)
12. [Design notes](#design-notes)
13. [Contributing](#contributing)
14. [About AKKO](#about-akko)

## How it works

```mermaid
flowchart LR
    Host["MCP host<br/>(Cursor, Claude, VS Code, an agent)"]
    IdP["Identity provider<br/>(OIDC, JWKS)"]
    Server["akko-mcp-trino"]
    Trino["Trino"]
    Policy["Policy engine<br/>(OPA, Ranger, built-in)"]
    Data[("Data sources")]

    Host -- "1. login" --> IdP
    Host -- "2. tool call + Bearer JWT<br/>+ X-Agent-Key" --> Server
    Server -. "verify signature<br/>against JWKS" .-> IdP
    Server -- "3. SQL as X-Trino-User=alice" --> Trino
    Trino -- "4. may alice read this?" --> Policy
    Trino -- "5. rows, masked and filtered for alice" --> Server
    Server -- "6. result" --> Host
    Trino --> Data
```

The user logs in to the identity provider the platform already has. The MCP
host sends the resulting JWT on every request. The server verifies it (signature
against the provider's JWKS, issuer, audience, expiry), reads the subject, and
runs the SQL in Trino **as that subject**. Trino asks its policy engine, applies
the user's catalog scope, row filters and column masks, and returns only what
the user could have read from any other client.

Two people asking the same question through the same server get two different
answers. That is the whole point.

## Compatibility

| | Supported | Tested |
|---|---|---|
| Python | 3.12, 3.13 | 3.12, 3.13 (CI) |
| Trino | 351 and later (the `X-Trino-User` protocol header) | 483, behind OPA |
| Policy engines | OPA (`trino-opa`), Ranger (Trino plugin), Trino file-based access control | OPA with row filters and column masks |
| Identity providers | any OIDC provider publishing a JWKS | Keycloak 26 |
| MCP | protocol 2025-03-26 via the official `mcp` SDK 1.8; transports `sse` and `streamable-http` | both transports, official SDK client |
| MCP hosts | anything that sends a bearer header: Cursor, Claude Desktop, VS Code, the Python SDK | Python SDK, the example agent |
| Models | any, the server never talks to a model | Mistral Small 3.2 through OpenRouter, driving the tools |

## Prerequisites

| You need | Why | Notes |
|---|---|---|
| Python 3.12 or later | runtime | `pip` and a virtual environment |
| A reachable Trino coordinator | the engine | HTTP or HTTPS, any recent version (tested on 483) |
| Trino configured to trust `X-Trino-User` from this server | identity forwarding | the server connects as `TRINO_USER` and sets `X-Trino-User` to the caller; `TRINO_USER` must be allowed to impersonate in your access control (`impersonation` rules in file-based access control, or the equivalent in OPA / Ranger) |
| A policy engine deciding for Trino | the governance | OPA (`opa.policy.uri`), Ranger, or Trino file-based rules. Without one, every user reads everything |
| An OIDC provider with a JWKS endpoint | identity | Keycloak, Entra ID, Okta, Dex… Tokens must carry `iss`, `aud`, `exp`, and a subject (`preferred_username` or `sub`) |
| An MCP host that can send a bearer header | the client | Cursor, Claude Desktop, VS Code, or any client built on the `mcp` SDK |

Optional: a Prometheus to scrape `/metrics`, and — for revocation before
expiry — an RFC 7662 introspection endpoint with a client registered for this
server.

## Install and run

```bash
pip install akko-mcp-trino
```

Or from source:

```bash
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 .
```

The package installs a Python module (`akko_mcp_trino`) and a command
(`akko-mcp-trino`); `python -m akko_mcp_trino` is the same entrypoint.

Point it at your Trino and your identity provider, then serve:

```bash
export TRINO_HOST=trino.example.internal
export TRINO_PORT=8080
export TRINO_USER=mcp-trino            # the account that connects and impersonates
export TRINO_CATALOG=iceberg

export MCP_AUTH_ENABLED=true
export MCP_AUTH_REQUIRED=true          # refuse requests without a verified identity
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

python -m akko_mcp_trino
```

You should see:

```
INFO:__main__:serving transport=sse port=3000 auth=True strict=True
```

Check it is alive and can reach Trino:

```bash
curl -s localhost:3001/health   # {"status":"ok"}       never touches Trino
curl -s localhost:3001/ready    # {"status":"ready"}    runs a bounded SELECT 1
curl -s localhost:3001/metrics  # Prometheus exposition
```

To try it on a laptop **without** an identity provider, leave
`MCP_AUTH_ENABLED` unset: every query then runs as `TRINO_USER`. Never run it
that way where the data matters.

### Container

A [Dockerfile](Dockerfile) ships with the repository (non-root, health check,
OCI labels); releases publish the image to `ghcr.io/akko-p/akko-mcp-trino`.
Run it with the same environment variables, publishing ports `3000` (MCP) and
`3001` (health).

## Connect an MCP host

The host needs the user's access token from the identity provider. With
`MCP_RESOURCE_URL` set, hosts that implement OAuth discovery find the provider
on their own (see [Discovery](#discovery)); otherwise paste the token.

Cursor, Claude Desktop, VS Code (`mcp.json`):

```json
{
  "mcpServers": {
    "trino": {
      "url": "http://localhost:3000/sse",
      "headers": {
        "Authorization": "Bearer <the user's access token>",
        "X-Agent-Key": "<the key registered for this product, if any>"
      }
    }
  }
}
```

From Python, with the official SDK:

```python
import anyio
from mcp import ClientSession
from mcp.client.sse import sse_client

async def main(token: str):
    headers = {"Authorization": f"Bearer {token}", "X-Agent-Key": "my-agent-key"}
    async with sse_client("http://localhost:3000/sse", headers=headers) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool("execute_query", {"sql": "SELECT 1"})
            print(result.content[0].text)

anyio.run(main, "<token>")
```

Set `MCP_TRANSPORT=streamable-http` to serve `/mcp` instead of `/sse`; the
guard is the same on both.

## Use it from an agent

[`examples/agent.py`](examples/agent.py) is a complete agent in eighty lines:
any OpenAI-compatible model (Mistral on La Plateforme, through OpenRouter or
LiteLLM; or any other provider), the MCP tools, the user's token. The model
never sees the token; the server receives it on every tool call.

```bash
pip install akko-mcp-trino openai
export LLM_BASE_URL=https://api.mistral.ai/v1
export LLM_API_KEY=...
export LLM_MODEL=mistral-small-latest
export MCP_URL=http://localhost:3000/sse
export USER_TOKEN=<the user's access token>
python examples/agent.py "Which catalogs can I see, and what is in them?"
```

Run it twice with two users' tokens and compare. The
[examples](examples/README.md) folder has the details.

## The tools

| Tool | Arguments | Returns |
|---|---|---|
| `list_catalogs` | — | the catalogs the user can see |
| `list_schemas` | `catalog` | schemas in that catalog |
| `list_tables` | `catalog`, `schema` | tables in that schema |
| `describe_table` | `catalog`, `schema`, `table` | columns and types |
| `execute_query` | `sql` | `{"columns": [...], "rows": [...], "row_count": n}` |

Every identifier is validated (`[A-Za-z_][A-Za-z0-9_-]*`) before it is placed
in SQL. `execute_query` accepts any SQL Trino accepts, **as long as it is a
read**: the statement is parsed into an AST and refused if it is more than one
statement, or if an `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `CREATE`, `DROP`,
`ALTER`, `GRANT`, `CALL` or `SET` appears anywhere in the tree — including
inside a CTE or a subquery. Results are capped at `TRINO_MAX_ROWS`.

Errors come back to the agent as `{"error": "..."}`; a permission refusal from
Trino is an ordinary error, not a crash.

## What happens on a request

```mermaid
sequenceDiagram
    participant H as MCP host
    participant G as Guard (ASGI middleware)
    participant T as Tool
    participant Tr as Trino

    H->>G: POST /messages  Authorization, X-Agent-Key, X-Request-Id
    G->>G: request id: honoured or generated
    alt agent products registered and key missing or unknown
        G-->>H: 401  X-Reason: agent_key_missing | agent_key_unknown
    end
    G->>G: verify JWT (JWKS, iss, aud, exp) → Principal(subject, jti)
    alt no verified identity and MCP_AUTH_REQUIRED
        G-->>H: 401  WWW-Authenticate: Bearer resource_metadata=…  X-Reason: unauthenticated
    end
    opt MCP_INTROSPECTION_URL set
        G->>G: RFC 7662: is the token still active? (cached by jti)
        G-->>H: 401 revoked · 503 introspection_unavailable
    end
    opt rate limits set
        G-->>H: 429  Retry-After  X-Reason: rate_limited
    end
    G->>T: Principal and agent in ContextVars
    T->>T: validate identifiers · refuse writes
    T->>Tr: SQL with X-Trino-User = subject
    Tr-->>T: rows (masked, filtered by the policy engine)
    T->>G: audit line {request_id, tool, subject, agent, token_id, ok}
    G-->>H: result  X-Request-Id
```

Order matters. The agent key is checked before the token, so an unregistered
product never triggers a JWKS lookup. Quotas are counted after authentication,
so a forged token cannot consume the slot of the person it names. A refused
request consumes nothing.

## Configuration

Everything is read from the environment. Nothing is hardcoded.

### Trino

| Variable | Meaning | Default |
|---|---|---|
| `TRINO_HOST`, `TRINO_PORT` | the coordinator | `localhost`, `8080` |
| `TRINO_USER` | the account the server connects as; it impersonates the caller | `trino` |
| `TRINO_CATALOG` | default catalog | `system` |
| `TRINO_READ_ONLY` | refuse writes in `execute_query` | `true` |
| `TRINO_MAX_ROWS` | result cap | `100` |

### Identity

| Variable | Meaning | Default |
|---|---|---|
| `MCP_AUTH_ENABLED` | verify bearer tokens | `false` |
| `MCP_AUTH_REQUIRED` | refuse requests without a verified identity | `false` |
| `MCP_JWKS_URL` | the provider's JWKS endpoint | — (required when auth is enabled) |
| `MCP_OIDC_ISSUER`, `MCP_OIDC_AUDIENCE` | claims to enforce | — |
| `MCP_RESOURCE_URL` | public URL of this server; enables RFC 9728 discovery | — (off) |
| `MCP_AGENT_KEYS` | `name:key,name:key` — registered agent products; empty disables the check | — (off) |

Auth enabled without a JWKS URL refuses to start: an authentication layer that
cannot verify anything must not pretend to. A malformed `MCP_AGENT_KEYS` entry
refuses to start for the same reason.

### Guards

| Variable | Meaning | Default |
|---|---|---|
| `MCP_RATE_LIMIT_USER`, `MCP_RATE_LIMIT_AGENT` | requests per window per user and per agent product; `0` disables | `0`, `0` |
| `MCP_RATE_LIMIT_WINDOW_SECONDS` | the sliding window | `60` |
| `MCP_INTROSPECTION_URL` | RFC 7662 endpoint; enables the revocation check | — (off) |
| `MCP_INTROSPECTION_CLIENT_ID`, `MCP_INTROSPECTION_CLIENT_SECRET` | credentials the provider expects | — (required with the URL) |
| `MCP_INTROSPECTION_TTL_SECONDS` | how long a verdict is cached by `jti` | `30` |

### Serving

| Variable | Meaning | Default |
|---|---|---|
| `MCP_TRANSPORT` | `sse` or `streamable-http` | `sse` |
| `MCP_PORT`, `MCP_HEALTH_PORT` | listening ports | `3000`, `3001` |
| `MCP_SERVER_NAME` | name announced to hosts | `trino-mcp` |

## Operations

### Two principals

A workspace key from Cursor or Claude is not a user. When `MCP_AGENT_KEYS` is
set, every request must carry both:

| Principal | Header | Says | Enforced by |
|---|---|---|---|
| End user | `Authorization: Bearer <JWT>` | for whom the query runs | JWKS here, then the policy engine in Trino |
| Agent product | `X-Agent-Key: <key>` | which product is calling | the registry here, for quotas and audit |

### Discovery

With `MCP_RESOURCE_URL` set, the server publishes RFC 9728 metadata without a
token, and every `401` says where to go:

```bash
curl -s https://mcp.example.com/.well-known/oauth-protected-resource
# {"resource":"https://mcp.example.com","authorization_servers":["https://idp.example.com/realms/data"],"bearer_methods_supported":["header"]}

curl -si https://mcp.example.com/sse | grep -i -e www-auth -e x-reason
# WWW-Authenticate: Bearer realm="trino", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
# X-Reason: unauthenticated
```

`authorization_servers` stays empty unless `MCP_OIDC_ISSUER` is set; the server
never guesses a provider.

### Refusals

Every refusal carries `X-Reason` and the request id, never the token:

| Status | `X-Reason` | Meaning |
|---|---|---|
| 401 | `agent_key_missing`, `agent_key_unknown` | products are registered and the key is absent or wrong |
| 401 | `unauthenticated` | no verified identity in strict mode |
| 401 | `revoked` | the provider says the token is no longer active |
| 429 | `rate_limited` | a window is full; `Retry-After` says when |
| 503 | `introspection_unavailable` | the provider could not be asked; the guard fails closed |

### Audit

Every response carries `X-Request-Id` — the one the edge sent, or one the
server generated. Every tool call writes one JSON line to the `mcp.audit`
logger keyed by that id:

```json
{"request_id":"edge-42","tool":"execute_query","subject":"alice","agent":"cursor","token_id":"jti-9","ok":false,"error":"Access Denied: Cannot select from columns [email] in table customers"}
```

`token_id` is the JWT's `jti`. The record has no field that could hold the
bearer. Join it with your gateway logs and Trino's query log on the request id.

### Revocation before expiry

A signature and an `exp` prove a token *was* valid. With `MCP_INTROSPECTION_URL`
set, the guard asks the provider (RFC 7662) and refuses a token whose `active`
is false. Verdicts are cached by `jti` for `MCP_INTROSPECTION_TTL_SECONDS`. If
the provider cannot answer, the request is refused with `503`, because
"unknown" is not "still valid".

Keycloak note: introspection is accepted only from a client that appears in the
token's `aud`. Register this server as its own confidential client and add it
to the audience mapper; the client that issued the token is not enough.

### Quotas

Two in-memory sliding windows, one per user subject and one per agent product.
Counters live in the process: with several replicas the quota is per replica.

### Health and metrics

`/health` on the health port never touches Trino, so a liveness probe cannot
kill the server because Trino is slow. `/ready` runs a bounded `SELECT 1`.
`/metrics` exposes `mcp_trino_queries_total`, `mcp_trino_query_errors_total`
and `mcp_trino_query_duration_seconds`.

## Development

```bash
pip install -e ".[dev]"
ruff check akko_mcp_trino tests && ruff format akko_mcp_trino tests
bash lint-vendor-neutral.sh   # fails if the package imports anything product-specific
pytest                        # 173 tests; coverage below 100 % fails the run
python -m build && twine check dist/*
```

CI runs the same steps on Python 3.12 and 3.13, then builds the container
image. A `v*` tag publishes the package to PyPI (trusted publishing) and the
image to GHCR — see [release.yml](.github/workflows/release.yml).

Every module in `akko_mcp_trino/` has one responsibility and its own test file:

| Module | Responsibility |
|---|---|
| `config.py` | read the environment, neutral defaults |
| `auth.py` | JWT verification against JWKS, `Principal` |
| `identity.py`, `agents.py` | user and agent product in ContextVars |
| `middleware.py` | the guard: key, token, revocation, quotas, discovery, request id |
| `discovery.py`, `ratelimit.py`, `revocation.py`, `audit.py` | one guard each |
| `sql_guard.py` | identifier validation, read-only decision on the AST |
| `tools.py`, `trino_client.py` | the five tools, the Trino connection |
| `server.py`, `app.py`, `__main__.py` | assembly, transport, entrypoint |

The README is tested too: every variable listed here is read by `config.py`,
and the defaults stated here are the defaults in the code.

## Design notes

**The guard is a pure ASGI middleware, not `BaseHTTPMiddleware`.** The latter
runs the downstream in a separate task and breaks ContextVar propagation: the
identity would never reach the tools. This was verified with concurrent
sessions of two users on both transports — zero crossed responses.

**Mounting is transport-independent.** One code path builds the transport app
and mounts the guard, whatever the transport. The guard once lived only on the
branch the default transport never took; a test now asserts it for both.

**Read-only is decided on the AST, anywhere in the tree.** A leading-keyword
check lets `WITH w AS (DELETE FROM t) SELECT 1` through. So did a root-node
check, until a live adversarial proof caught it. The guard now walks the whole
tree.

**Identity comes from the token, never from a header.** A caller sending
`X-Trino-User` or `X-Forwarded-User` changes nothing; the subject forwarded to
Trino is the verified JWT subject.

**The server does not decide access.** It carries identity. Putting policy in
the server would duplicate — and eventually contradict — what the engine
already enforces for every other client.

## Contributing

Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for the
ground rules (tests first, 100 % coverage, product-neutral, fail closed) and
[SECURITY.md](SECURITY.md) for reporting a vulnerability privately.
Changes are tracked in [CHANGELOG.md](CHANGELOG.md).

## About AKKO

akko-mcp-trino is built and maintained by [AKKO](https://akko-ai.com), a
French company working on governed access for AI agents to enterprise data,
in place, on the engines and identity providers customers already run. This
server is the first brick of that work, extracted from the AKKO platform where
it has run in production since July 2026 and released so that anyone running
Trino can put identity in front of their agents today.

If you run Trino behind Ranger or OPA and want a hand wiring this up, or want
to see the rest of the platform, [say hello](https://akko-ai.com).

**Author:** Abderrahmen Dridi ([@ab2dridi](https://github.com/ab2dridi)) ·
**Issues:** [github.com/AKKO-p/akko-mcp-trino/issues](https://github.com/AKKO-p/akko-mcp-trino/issues)

## License

Apache 2.0. See [LICENSE](LICENSE).
