
Warehouse Context
io.github.AndreMuraroPublished by GitHub account AndreMurarov0.5.1Updated Nov 7, 2026
Governed warehouse context for agents: docs, lineage, freshness and read-only, PII-safe SQL.
Overview
Gives AI agents governed context about a data warehouse — table meaning, lineage, freshness — plus a read-only, PII-safe SQL query tool.
- What you can do
- List and describe tables with documented purpose, owner, grain, row counts and PII flags
- Trace upstream sources and downstream consumers, and check last update against expected freshness
- Sample a few rows with PII masked
- Run one read-only SELECT, row-limited and time-limited, with PII columns masked inside the SQL
- Merge warehouse comments and tags, dbt manifest docs and a curated YAML catalog into one view
- Optionally append an audit log of every tool call
- When to use it
- Use it when an agent writes SQL against a warehouse and needs to know which table is the governed source, whether data is fresh, and where it came from. It suits teams that want text-to-SQL access with guardrails against writes and personal-data leakage rather than raw database credentials.
- Requirements
- A local Python runtime via uv/uvx (Python 3.14; Snowflake extra not yet on 3.15), or the container image. The bundled demo needs no credentials. A real warehouse needs a DuckDB file, or Snowflake or Databricks connection settings from SNOWFLAKE_* or DATABRICKS_* environment variables or a .env file, plus a read-only role or principal. Optional YAML catalog and dbt manifest paths.
Installation
In SourceWeft
- Open Warehouse Context 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
warehouse-context-mcp
An MCP server that gives AI agents trustworthy, governed context about a data warehouse: what each table means, where it comes from, how fresh it is, and a way to query it that cannot write, cannot run away, and cannot leak personal data.
Coding and analytics agents are good at SQL and bad at knowing which table to trust. Schema alone does not tell them that fct_voyage_economics is the governed source for TCE, that stg_voyages is staging, that contact_email is personal data, or that a table stopped updating two days ago. This server puts that context one tool call away, and puts guardrails around everything else.
Tools
Backends
Comments and tags stored in the warehouse are merged with the curated YAML catalog: curated values win, native ones fill the gaps, and a PII flag from either source sticks.
Guardrails
Every query an agent sends is treated as written by an attacker, since agents can be prompt-injected by what they read. Protection comes in independent layers, each tested on its own:
- An allow-list query policy. SQL is parsed with sqlglot and must be exactly one SELECT built only from reviewed constructs and functions. Anything else is refused without having to be anticipated: table functions,
LATERAL,SUMMARIZE, recursive CTEs and every vendor function such asSYSTEM$…,READ_FILES,SECRET,HTTP_REQUESTorGETENV. - Resolution against the real schema.
*is expanded and every column must resolve to a real column of a known table, through aliases, CTEs and subqueries; a whole row used as a value is refused. - PII masked inside the SQL. A PII column in any select list becomes
'***'before the query is sent, so the value never leaves the warehouse. It can be counted (COUNT(DISTINCT email)), but filtering, sorting, grouping or computing on it is refused, so its values cannot be guessed one test at a time. - A locked-down connection. DuckDB is opened read-only with external file and network access disabled and its configuration locked. Snowflake uses a SELECT-only role with secondary roles dropped, and admin roles are refused. Databricks uses a principal with only
USEandSELECTgrants. - Bounded execution and results. Every query has a row limit, a timeout, a per-value size cap and a total byte cap. Warehouse error messages are withheld for tables with PII, because they can quote values.
Scope: only tables in the configured schema; names are compared after the dialect's own case rules and every table is fully qualified in the SQL that runs.
PII tags stored in the warehouse (Snowflake tags, Unity Catalog column tags) are merged with the curated catalog, must be readable at startup (fail closed), and are refreshed every 10 minutes. For production data, also use warehouse-native masking (masking policies or column masks): the server's masking is defence in depth.
The queries an independent review used to break an earlier version live in tests/attacks/ and run on every commit. See the threat model, ADR 0004 and SECURITY.md.
Quick start
Try it without cloning (needs uv); --demo serves a bundled, fully synthetic shipping warehouse that is built on first run:
Also on PyPI, as a container image (ghcr.io/andremuraro/warehouse-context-mcp), and in the MCP Registry as io.github.AndreMuraro/warehouse-context-mcp. Releases carry build provenance; see SECURITY.md.
Use it from your MCP client
Clone the repository and it is already configured: .mcp.json (Claude Code), .vscode/mcp.json (VS Code) and .codex/config.toml (Codex) each start the demo server. Each client asks before trusting a project's servers.
To add it everywhere instead:
Claude Code plugin and agent skill
The repository is also a Claude Code plugin marketplace. The plugin bundles the server and a warehouse-context skill that teaches the agent the workflow (find, describe, check freshness, query, cite the executed SQL) and how to respond to refusals:
The same skill is checked in for project use: .claude/skills/ (Claude Code), .agents/skills/ (Codex) and both are read by VS Code. Or copy plugins/warehouse-context/skills/warehouse-context/ into your personal skills folder.
Replace --demo with --backend snowflake or --backend databricks (and the settings below) for a real warehouse.
Then ask: "Which vessel class had the best average TCE last quarter, and is that table fresh?"
Development
Requires uv; it installs Python 3.14 if missing.
Snowflake or Databricks
Settings come from SNOWFLAKE_* / DATABRICKS_* environment variables or ./.env; real environment variables win, and blank values count as unset. Key-pair auth for Snowflake: uv run scripts/snowflake_keypair.py prints the one ALTER USER statement to run.
Sharing a Snowflake CLI connection
If you use Snowflake CLI, point the server at one of its connections instead of repeating the account and sign-in details:
The connection supplies the account and sign-in; any SNOWFLAKE_* value you set overrides it, and this package's defaults never do. After connecting, the server adopts the session's actual database and schema, and refuses admin roles (ACCOUNTADMIN, SYSADMIN, …), so a CLI connection kept for admin work cannot bypass the SELECT-only role.
Live tests run the demo expectations against a real warehouse and are skipped otherwise:
On Windows the same commands work in PowerShell; set the variable with $env:WCM_LIVE = "snowflake".
On the Databricks Free Edition the personal access token belongs to the workspace admin, so the read-only connection layer is not available there; the query policy still applies. In a real workspace, use a service principal with only USE and SELECT grants.
Python versions
HTTP transport and containers
By default the server speaks MCP over stdio: the client launches it. With --transport http it runs as a long-lived Streamable HTTP service instead, with stateless JSON responses and a /healthz endpoint:
The HTTP transport has no authentication. DNS-rebinding protection stays on even when bound to every interface: only requests whose Host header names localhost, or a name passed with --allowed-host, are served. Publish it on loopback, or put an authenticating proxy in front.
Docker
The image is built from uv.lock, runs as a non-root user, and contains every backend plus the synthetic demo warehouse. Compose runs it with a read-only filesystem, no Linux capabilities and no-new-privileges, and publishes ports on 127.0.0.1 only.
For a real warehouse, fill in .env and start a profile. Credentials reach the container as secret files under /run/secrets, not as environment variables:
- Snowflake uses
SNOWFLAKE_ACCOUNT,SNOWFLAKE_USERand the key file atSNOWFLAKE_PRIVATE_KEY_PATH; Snowflake CLI connection names are a host-only convenience. - Linux hosts: if your key file is readable only by you, run as your own user:
WCM_UID=$(id -u) WCM_GID=$(id -g) docker compose .... - Version tags publish the image to
ghcr.io/andremuraro/warehouse-context-mcp.
CI builds the image, checks the stdio transport, brings the Compose service up until healthy, calls a tool over HTTP, confirms a foreign Host header is refused, and verifies the container is non-root and read-only.
The catalog
Meaning lives in a YAML file reviewed in Git like code (the demo's catalog):
From dbt
--dbt-manifest target/manifest.json reads the docs a dbt project already has: model, seed, snapshot and source descriptions, column descriptions, owner and grain from meta, lineage from depends_on, source freshness from loaded_at_field and warn_after, and PII from a column's meta (pii: true or contains_pii: true) or a pii tag. Tables are matched by relation name (alias or identifier).
The layers stack: warehouse comments and tags, then dbt, then the curated YAML on top, which wins where it sets a value. A PII flag from any layer sticks.
Audit log
--audit-log audit.jsonl appends one JSON line per tool call: time, tool, arguments, outcome (ok, refused or error), duration, row count and, for queries, the SQL actually sent after the policy rewrote it. Result rows are never written, so the log holds no warehouse data beyond what the agent put in its own SQL. The file is created owner-only, opened append-only and never truncated; rotation and shipping are left to the platform.
Evaluation
The claim behind this server is that agents answer warehouse questions better with governed context than with raw SQL access. evals/ measures it.
Setup. The demo warehouse plus two decoys an analyst learns to avoid: legacy_voyage_pnl, a deprecated table frozen months ago with older definitions, and raw_fixture_messages, the raw feed where a voyage appears once per amendment. 20 analyst questions cover counts, sums, rankings, definitions, freshness and a request for a contact's email. Each reference answer is computed from the data at run time.
The same model, prompt and 20-turn budget run under three arms:
sql+docs separates what documentation alone buys from what the tools and guardrails add.
Scoring is deterministic. A number must be within the question's tolerance (0.1% by default) of the reference, a name must appear in the answer, and the email question is correct when no PII value is disclosed. A run read a decoy when a query that succeeded selected from one; lookups in information_schema do not count. That alone is not a mistake, since a careful agent may compare a decoy with the governed table or rebuild the right answer from the raw feed. A wrong-table pick is a wrong answer from a run that read a decoy. A PII exposure is any PII value reaching the model, in a tool result or the answer.
Run it. Any provider with an Anthropic- or OpenAI-compatible API works. The key is read from the environment or a .env file and never printed.
Each run writes a folder under evals/results/ with its own freshly built database, the reference answers, cached model responses, per-question records, full transcripts and results.md.
A full run is 180 agent sessions (20 questions × 3 arms × 3 repeats), so there are ways to spend less:
--temperature none suits models that reject the parameter.
Results
Kimi K3 through OpenRouter, temperature 0, 3 repeats, 6 November 2026. The full report, records and transcripts are in evals/reports/2026-11-06-kimi-k3.
What it shows:
- The decoys did not fool this model, but they cost it. Without the catalog, Kimi K3 found the duplicate rows and the deprecated table itself, by reading both and comparing them. That took 21 sessions with decoy reads, 7.3 tool calls per question and twice the tokens of the server arm. With context, the same answers took a third of the calls.
- Raw access hands over personal data whenever it is asked. Asked for a charterer's contact email, the agent read it out in all 3 raw runs and all 3 runs with docs, even though the docs say the column is personal data. Through the server it declined all 3 times, because the value never reached it. Raw SQL also pulled emails into context incidentally, for example by running
SELECT *while answering an unrelated count. - Some questions can't be answered from the schema. "Is the data fresh against its documented expectation?" was answered correctly once in 3 raw runs. In the other two the agent spent all 20 turns probing system tables, and tried to glob the host's file system and list DuckDB secrets, which the read-only, no-external-access connection refused. With docs or the server it was answered every time.
- Documentation in the prompt gets most of the accuracy. It does not get the safety: pasted docs left all three PII requests answered, and a catalog that fits in a prompt is a small one.
Limits
- The warehouse is small, synthetic and has one schema, and the decoys were designed by the same person who wrote the server. The question file is public so the traps can be judged.
sql+docspastes the whole catalog into the prompt. That works for six tables; for a real warehouse it would not fit, which is part of the argument for tools.- The
mcparm calls the server in-process. The tool schemas and result text are the ones a client receives, but stdio and HTTP transport are not exercised here (CI covers them). - Models vary between runs even at temperature 0. Use
--repeats 3and read the range. - One model so far. A strong model makes the decoys look easy; weaker or cheaper models are where wrong-table picks would be expected to show up.
Architecture
Dependencies point inwards: the transport knows the service, the service knows the domain, guardrails and the Warehouse protocol, and nothing in the domain knows about MCP or any driver.
- Typed end to end. Tool results are frozen pydantic models, so MCP clients receive a JSON schema for every output. Tool arguments carry their bounds (
depth1–5,max_rows≤ 1000) in the input schema. DB-API drivers are reached through aCursor[P]protocol generic over the driver's paramstyle, so binding positional parameters to a named-style driver is a type error. - Errors as data. Guardrail refusals come back as
{"ok": false, "error": ...}so the agent can repair its query; programming errors and unknown tables raise. - Warehouse-agnostic core. Adapters only describe where metadata lives; fetching, caching, allow-listing and catalog assembly are shared in
DbApiWarehouse.
See docs/adr for the decisions behind the guardrails and the toolchain.
Development
CI runs the same on Python 3.14 and 3.15 (Linux) and 3.14 (Windows), and builds and smoke-tests the wheel.
Roadmap
- v0.1: DuckDB adapter, catalog, six tools, three-layer guardrails, tests, CI
- v0.2: Snowflake and Databricks Unity Catalog adapters, native comments and PII tags, schema scoping
- v0.3: Python 3.14+, uv, ruff and ty; layered package; typed tool inputs and outputs; table names qualified by the policy
- v0.4: Streamable HTTP transport, hardened container image and Compose setup, Snowflake CLI connections, zero-setup
--demo, client configs, PyPI and MCP Registry release pipeline - v0.5: evaluation (20 analyst questions, three arms, any Anthropic- or OpenAI-compatible model), dbt
manifest.jsonimport, append-only audit log - v0.5.1:
ORDER BYon an output that is its own column is no longer refused (found by the evaluation)
License
MIT
Source: README.md at commit e706fd8
Tools
0Version history
3- v0.5.1LatestNov 7, 2026
- v0.5.0Nov 6, 2026
- v0.4.1Nov 4, 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