Semantic Context MCP
An MCP server that hands a model the meaning of a data warehouse alongside its structure.
A model with schema access sees tbl_rev_fnl_v2 and a column called amt. It does not knowwhether that is gross or net, cents or dollars, maintained or abandoned. So it guesses, and theguess runs and returns a plausible number. This server closes that gap.
Four tools:
| Tool | Answers |
|---|---|
search_tables |
"Which table holds daily net revenue?" |
describe_table |
"What does one row mean, and what unit is this column in?" |
trace_lineage |
"Where did this number come from, and what breaks if I change it?" |
check_health |
"Is this table safe to rely on right now?" |
Status
Phase 3 of 6 complete — see PLAN.md.
All four tools are backed by real, derived metadata — nothing any tool returns is hand-typed exceptthe four table descriptions and meta blocks disclosed below. search_tables is hybrid: SQLite FTS5(BM25 keyword ranking) fused with cosine similarity over MiniLM description embeddings viareciprocal rank fusion. Freshness/row-count/null-rate fields and column-level lineage are stillNone/model-level-only until Phases 4–5 add live warehouse profiling and sqlglot resolutionrespectively — a known, tracked gap, not a bug.
One thing worth knowing before you connect this to Claude Desktop: search_tables' semantichalf needs the search extra installed in the server's own environment, not just at ingestiontime — see "Setup" below. Without it, search_tables still works, just as keyword-only BM25.
Derived vs. authored
The credibility of this project rests on where the metadata comes from, so it is stated up frontand kept accurate as phases land.
| Signal | Source | Human-authored? | Status |
|---|---|---|---|
| Table purpose, grain, materialization | manifest.json |
No | Live |
| Test pass/fail | run_results.build.json (see note below) |
No | Live |
| Model-level lineage | manifest.json's depends_on |
No | Live |
| Table search (keyword) | SQLite FTS5 / BM25 over name+description+columns | No | Live |
| Table search (semantic) | Cosine similarity over MiniLM embeddings | No | Live (needs search extra) |
| Column-level lineage | sqlglot over compiled SQL |
No | Phase 5 |
| Freshness, row counts, null rates | Live DuckDB queries | No | Phase 4 |
| Ownership | dbt meta.owner, else git blame on the model file |
Partly | Live |
| Column descriptions and units | dbt schema.yml |
Yes — same input any dbt project already has | Live |
| Deprecation status + successor | dbt meta.status / meta.superseded_by |
Yes | Live |
Not run_results.json, deliberately: dbt docs generate overwrites that file with acompile-only record where every node reports "success" — it isn't running tests, justintrospecting the built warehouse. Read naively, every table would look like it always passes,permanently. fixture/build_warehouse.py runs dbt build first,preserves its output as run_results.build.json, then runs dbt docs generate forcatalog.json — see that script's docstring for the full explanation.
The demo warehouse is dbt-labs/jaffle_shop_duckdbwith four added "decoy" models. jaffle_shop is too clean to show ambiguity; the decoys reproducewhat real warehouses look like. They are synthetic and labelled as such — the pipeline thatderives metadata from them is not.
Requirements
Python 3.12+ and uv. No GPU. The embedding model(all-MiniLM-L6-v2) itself is ~80MB and runs on CPU, but it's loaded via sentence-transformers,which pulls PyTorch — budget ~2GB of disk for the search extra below.
Network note: the first search_tables call in a server process downloads the model fromHugging Face Hub if it isn't cached yet, and — even once cached — sentence-transformers stillmakes a few HEAD requests to check for updates on that first call. This happens once per process(the loaded model is cached for the process's lifetime, not reloaded per query), but it does meanthe first search after startup needs network access. Set HF_HUB_OFFLINE=1 in the server'senvironment to skip the freshness check once the model is already cached locally.
Setup
uv sync
uv sync installs only the server, and runs a working server: describe_table, trace_lineage,and check_health are fully live, and search_tables works too — in BM25 (keyword) mode, withoutsemantic ranking. Heavier extras are opt-in so the common path stays fast:
uv sync --extra search # sentence-transformers + numpy — turns on semantic search_tables ranking.
# Needed in the SERVER's own environment, not just at ingestion time: the
# query is embedded live, on every search_tables call.
uv sync --extra ingest # sqlglot, duckdb — ingestion/fixture-building only, never loaded by the
# running server
uv sync --extra dbt # dbt-core, dbt-duckdb — only needed to build the fixture warehouse
uv sync --extra eval # anthropic, pyyaml
Build the fixture warehouse
Needed once (and any time you want a fresh "yesterday") before the server has real data to serve —without it, every tool call fails with a CatalogUnavailableError telling you to run this.
uv sync --extra dbt --extra ingest --extra search # omit --extra search to skip embeddings —
# search_tables then serves BM25-only
uv run python fixture/build_warehouse.py # seed -> dbt build -> dbt docs generate
uv run python -m semantic_context_mcp.ingest # manifest + catalog + run_results -> catalog.db
build_warehouse.py regenerates the seed dates so the healthy table's data ends "yesterday" andthe intentionally-stale decoy table (tbl_rev_fnl_v2) stops 40 days before that — re-run it anytime to keep the demo's freshness story accurate against the current date. Re-running the wholesequence is always safe: both scripts fully replace their outputs rather than merging into them.
Run
uv run semantic-context-mcp
The server speaks MCP over stdio and will sit silently waiting for a client — that is correctbehavior, not a hang. To exercise it without a client:
uv run python scripts/smoke_stdio.py
Connect to Claude Desktop
Add to claude_desktop_config.json:
- Windows:
%APPDATA%\Claude\claude_desktop_config.json - macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"semantic-context": {
"command": "uv",
"args": ["--directory", "C:\\Users\\asus\\Desktop\\projects\\semantic-context-mcp", "run", "semantic-context-mcp"]
}
}
}
Restart Claude Desktop, then ask "What's our daily revenue table?"
Layout
fixture/
jaffle_shop_duckdb/ Cloned dbt project + models/decoys/*.sql (the 4 tables from PLAN.md §3)
pristine_seeds/ Untouched original seed CSVs -- prepare_seeds.py's source of truth
prepare_seeds.py Shifts seed dates to end "yesterday", idempotently
build_warehouse.py seed -> dbt build -> dbt docs generate, in the order that keeps both
run_results.json's test outcomes and catalog.json's real columns valid
src/semantic_context_mcp/
models.py Pydantic contract for all four tool returns
errors.py Errors written to be read by the model
server.py MCP adapter — no logic
core/ The actual logic; also what the eval harness calls (core.STILL_STUBBED tracks
which modules are not yet backed by real data)
ingest/ dbt artifacts + git + SQLite -> catalog.db (artifacts.py, ownership.py, store.py)
eval/ Phase 6: three-arm measurement harness
scripts/
smoke_stdio.py Drives the real server over the real transport
tests/ Contract, consistency, ingestion, and stdout-purity guards
core/ is deliberately transport-agnostic: the MCP server and the eval harness are both thinadapters over it, so the eval cannot drift from what the server actually does.