
Lagaam
io.github.lagaam-aiPublished by GitHub account lagaam-aiv1.0.0Updated Nov 7, 2026
Stops an agent's Trino or Pinot query before it runs if it costs too much or reads outside its grant
Overview
Guarded MCP gateway that lets an AI agent query Trino or Apache Pinot, refusing queries that exceed cost budgets or read outside its table grant.
- What you can do
- List catalogs and describe tables, scoped to the agent's table grant
- Run read-only SELECT queries against Trino or Pinot, with a row cap and timeout
- Price each query from the engine's plan and block it before execution if it exceeds scan-byte or widest-step row budgets
- Reject DDL/DML, multi-statement input, SELECT * and table-function passthrough; inject a LIMIT when missing
- Warn on suspicious results such as zero rows, truncated pages or all-NULL columns
- Write one JSONL audit line per tool call, including the quote next to actual bytes read
- When to use it
- Use it when you want to give an AI agent SQL access to a shared Trino or Pinot cluster without letting a missing partition filter, cross join or SELECT * tie up other users' queries. It suits teams that need per-agent table allowlists, pre-execution cost gates and an audit trail.
- Requirements
- Runs locally over stdio as the PyPI package lagaam (Python, e.g. via uvx). LAGAAM_ALLOWED_TABLES is required or the server will not start. Needs a reachable Trino coordinator (TRINO_HOST, TRINO_PORT, TRINO_USER) or Pinot controller and broker URLs with LAGAAM_ENGINE=pinot. Optional login via TRINO_PASSWORD or TRINO_JWT, PINOT_USER and PINOT_PASSWORD, and TRINO_VERIFY for a private CA. Tables need statistics to be priced.
Installation
In SourceWeft
- Open Lagaam in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
[Lagaam — every query priced before it runs]
One agent query shouldn't stall everyone's Trino. On a shared cluster, a missing partition filter or an accidental cross join doesn't cost you a bill — it costs everyone else their queries. Lagaam is an MCP server that sits between your AI agents and your lakehouse (Trino and Apache Pinot): every query is schema-grounded, priced from the engine's own plan before it runs, checked against a budget, and audited — and the bad one is refused before it runs, with a fix the agent can act on.
[Lagaam demo: SELECT * rejected, an oversized join blocked pre-execution, a scoped query running]
Real session, real Trino, nothing mocked — reproduce it with
uv run --project server python examples/demo.py.
Real session, real Pinot 1.5.1 realtime table, nothing mocked — reproduce it
with uv run --project server python examples/demo_pinot.py.
The problem
Agents write syntactically-valid, catastrophic SQL, and on a shared cluster
the damage isn't theirs alone. A missing partition filter turns into a full
scan that ties up every worker; an accidental cross join fills worker memory
while everyone else's queries wait behind it; a retry loop runs it again. A
SELECT * drags 40 columns into a context window that needed 2. The usual
fix is to not give agents database access at all.
Lagaam gives them access with reins on:
- Cost is a quotation, not a bill. Every query is priced from the
engine's own plan before execution —
EXPLAIN (TYPE IO)for the bytes it would scan,EXPLAIN (TYPE LOGICAL)for the widest row count any operator would build (the number a cross join blows and aLIMITcannot hide). Over budget → blocked, with the number and the fix. - Un-estimable means no. No table statistics, a self-join that breaks the estimate, a passthrough the planner can't see — the gate fails safe instead of hoping.
- Read-only, enforced in the AST. Single
SELECTonly. No DDL/DML, no multi-statement injection, noSELECT *, no table-function passthrough, and aLIMITis injected when missing. Validated SQL is re-rendered, so what runs is exactly what was checked. - Agents ground themselves.
list_catalogsanddescribe_tablereturn exact names, types, and row estimates — scoped to the agent's table grant, so the agent never learns names it isn't allowed to touch. - Results are verified before they're trusted. Zero rows, truncated pages, all-NULL columns — the agent gets a warning with a next action, not a silently misleading answer.
- Every call is audited. One JSONL line per tool call: who, what, allowed or denied, and why. A query's line puts the quote next to what the engine says it actually read, so you can see how far off the estimate was before you loosen or tighten a budget.
Doesn't Trino already limit this?
It limits a query once it is running. Lagaam refuses it before it starts. Keep both.
Measured on Trino 476: SELECT o.orderkey, l.partkey FROM tpch.tiny.orders o CROSS JOIN tpch.tiny.lineitem l LIMIT 10 reads 676,575 bytes by Trino's own
plan — any scan cap above 0.7 MB lets it run — and would build 902,625,000
rows. Lagaam refuses it before it runs: "This query would build 902,625,000
rows at its widest step, over your budget of 50,000,000 … a LIMIT will not
help — join on a column with more distinct values, or filter each side
before the join."
Resource groups stay your backstop for everything that does reach the cluster; Lagaam is the gate in front of it for agent traffic.
What about other database MCP servers?
They're good servers that do a different job: keep the agent read-only and bound what comes back. Lagaam does that too, and also prices the query first.
Catch rate
11 queries an LLM agent plausibly writes — full scans, SELECT *, DDL,
injection attempts, oversized joins, out-of-grant reads. A raw MCP
wrapper submits all of them to the engine. Lagaam stops 11/11 before
execution while the well-scoped control query runs untouched.
Reproduce: benchmarks/catch_rate.py →
results.
Quickstart
Against the Trino you already have (TRINO_PORT / TRINO_USER if yours
aren't 8080 / lagaam):
Or try it on a demo warehouse:
For Pinot, --profile pinot brings up the batch quickstart and
--profile pinot-realtime up -d brings up a Kafka-fed streaming one —
run examples/pinot-realtime/bootstrap.sh after it to create the topics,
tables and feed. Then start the server with LAGAAM_ENGINE=pinot.
Wire it into any MCP client (Claude Code, Claude Desktop, or your own agent):
In Claude Code, install it as a plugin instead:
It asks for the tables to allow (LAGAAM_ALLOWED_TABLES) and your Trino
host, port and user (localhost / 8080 / lagaam if you leave them).
From a shell, pass them as flags:
On Pinot, pick engine=pinot and set the controller and broker URLs
(http://localhost:9000 / http://localhost:8000 if you leave them):
Other LAGAAM_* settings, the Trino login (TRINO_PASSWORD or TRINO_JWT,
plus TRINO_VERIFY for a private CA), and PINOT_USER / PINOT_PASSWORD for
a Pinot with auth, are read from the environment you start claude in, so a
password never sits in the plugin's config.
The agent gets three tools — list_catalogs, describe_table,
query_data — and cannot reach the engine any other way.
Configuration
A shared Trino with a login works the same way. Set TRINO_PASSWORD (or
TRINO_JWT) and lagaam talks https, since Trino won't take a password over
plain http; set TRINO_PORT to the cluster's https port too (often 443 or
8443). A login the cluster refuses tells the agent so, without the password,
and a password and a JWT both set stop the server at startup rather than
guessing which one you meant. Try it locally against a Trino that wants both:
examples/trino-auth/setup.sh, then
docker compose -f examples/docker-compose.yml --profile trino-auth up -d.
The server will not start without LAGAAM_ALLOWED_TABLES. An agent that
can reach every table in every catalog is the thing this exists to prevent, so
that has to be asked for — set LAGAAM_ALLOW_ALL_TABLES=true if you mean it.
The budget dimensions above apply whether or not you set them. Leaving
LAGAAM_MAX_SCAN_BYTES and LAGAAM_MAX_INTERMEDIATE_ROWS unset gives you
their defaults, not an open gate — an unconfigured server refuses what it
cannot afford rather than waving it through. If queries are being denied and
you expected no limits, that is why; raise the dimension you mean to raise.
Cost quotes come from the engine's own plan estimates, and those need table
statistics: a table without stats cannot be priced, so its queries are refused
rather than guessed at. Run ANALYZE on your tables before pointing an agent
at them — a connector that has no statistics at all is effectively unusable
through the gate. That is deliberate: a query nobody can size is exactly the
kind that stalls a shared cluster.
What it doesn't do
- One agent per server process, over stdio. Identity and grant come from
the environment at startup; there is no HTTP transport or per-request token.
Run one
lagaamper agent. - No stats, no answer. A table without statistics can't be priced, so its queries are refused (see above).
- Per query, not per day. Budgets bound each query; there is no running total across an agent's session.
- Trino and Pinot only. Other engines would need their own adapter behind
the same
QueryEngineport. - Actuals are recorded, never enforced. The audit line shows what a query
really read next to its quote; nothing is cut off mid-run on those numbers —
that is what
LAGAAM_QUERY_TIMEOUTand your engine's own limits are for.
How it works
A QueryEngine port with a Trino adapter and a native Pinot adapter —
grounding, execution, and a synthesised cost quotation for both OFFLINE and
REALTIME tables. Every query_data call walks one pipeline: validate
(sqlglot AST) → table allowlist → cost quotation → budget gate → execute
(row cap + timeout) → verify → audit.
On Trino, the quote is the engine's own plan, not a guess from the SQL text:
EXPLAIN (TYPE IO) prices the bytes a query would scan, and
EXPLAIN (TYPE LOGICAL) prices the widest row count any operator would
build — the number a cross join blows and a LIMIT cannot hide.
Pinot gives no such plan — every table scan reports the same placeholder row count whatever the table holds, and no endpoint reports bytes at all — so its quote is synthesised instead: from static segment metadata, the broker's own pruning oracle (how many segments survive the predicate), and the plan's join/union shape. A consuming (REALTIME) segment, which the controller reports as empty mid-flight, is priced at the stream's own flush threshold instead of zero. A join is charged its bound rather than the product wherever the catalog can prove the join key. See ADR 0008 and ADR 0009.
Details in docs/architecture.md; the longer story in docs/vision.md.
Status
v1.0.0 — on PyPI as lagaam, listed in the MCP Registry as io.github.lagaam-ai/lagaam, installable as a Claude Code plugin (lagaam@lagaam). Trino adapter, schema tools, plan-based cost guard, query
budgets, read-only enforcement, per-agent allowlists, result verification,
audit log with post-execution actuals, https + password/JWT login for a shared
Trino. 1,243 unit + 219 integration tests (live Trino 476 and Pinot
1.5.1, batch and realtime), mypy strict. LAGAAM_ENGINE=pinot starts the
native Pinot adapter: grounding, execution and a quotation synthesised from
segment metadata and the broker's own pruning oracle, for OFFLINE and
REALTIME tables alike — a consuming segment is charged at the stream's
flush threshold, and a join is charged its bound rather than the product
wherever the catalog proves the key.
The MCP server is feature-complete: what's here is what v1 is, and new work is bug fixes and engine compatibility. The Kubernetes control plane in the roadmap is not scheduled.
Apache 2.0.
Source: README.md at commit b176ec5
Tools
0Version history
3- v1.0.0LatestNov 7, 2026
- v0.2.5Oct 10, 2026
- v0.2.4Sep 29, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts