auto-ontology-mcp
Health Pass
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 49 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
NVIDIA Auto Ontology's semantic layer and a personal knowledge base as an MCP server for Claude, ChatGPT and other MCP clients. No LLM API key needed.
Auto Ontology MCP (auto-ontology-mcp) brings NVIDIA Auto Ontology's semantic layer to any MCP client, for two jobs:
- Ask your databases. Point it at a database, and the model you already use (Claude Code, Codex, OpenClaw, Hermes Agent, Cursor, VS Code, or the Claude and ChatGPT apps) builds business Terms, reviews joins and answers questions with SQL that the server checks against the ontology and the data.
- Keep a personal knowledge base. Share receipts, statements, papers, book notes or screenshots with your assistant; it reads them and stores the facts as records, the ideas as linked concepts, the passages worth finding again in a full-text index, and where each came from. The full originals stay in your Obsidian vault. Later questions get exact numbers, searchable quotes and links back to the original.
[!TIP]
Use it in place of an LLM Wiki. In Karpathy's LLM Wiki pattern the model compiles your sources into markdown pages that you read in Obsidian. Auto Ontology MCP runs the same loop (ingest, query, lint, with an index and a log) on an ontology instead of loose pages: facts go into typed tables you can sum, filter and compare with SQL, ideas become concept pages with typed links, every record, page and link keeps its source and the quote behind it, andexport_wikiwrites it all out as an Obsidian vault with aliases,[[links]]and the graph view. How the two map
Auto Ontology runs its own LLMs (NVIDIA NIM) behind a Next.js, FastAPI and Postgres stack. This project hands every language step (reading, extracting, naming, judging, writing SQL) to the model in your MCP client, so there is no model endpoint to host and no API key to configure. The server keeps the deterministic work: schema extraction, profiling, key inference, validation, storage and read-only execution, in local SQLite files.
Why
Text-to-SQL fails quietly. The query runs, a number comes back, and nothing says it is wrong.
| ❌ A model reading a raw schema | ✅ The same model with auto-ontology-mcp |
|---|---|
Guesses which of billing_city_id and delivery_city_id "the customer's state" means |
Gets the join path from reviewed relationships, with the default role and the alternatives spelled out |
Joins customers.customer_id = orders.order_id because the integers happen to overlap |
validate_sql rejects the join and names the one the ontology knows |
Filters state_name = 'pennsylvania' and reports zero revenue |
Is told the stored value is 'Pennsylvania' before running the query |
Writes discount_pct > 20 against a column that holds 0 to 0.3 |
Is asked whether it meant 0.2 |
| Sums a product price after joining to order lines and inflates the total | Gets a fan-out warning for the one-to-many join |
| Answers "return on ad spend" with whatever revenue column it finds | Hears from check_answerable that nothing covers ad spend, and says so |
See it in action
auto-ontology-mcp demo sets up three demos about one fictional store: a synthetic retail database (11 tables, 3,200 orders), three of the store's documents in the knowledge store (a meeting note, a tag guide and a quality digest, with their originals in the Obsidian vault), and questions that need both. Tool output below is abridged from real runs of the test fixtures (tests/test_demo.py); the model's replies are illustrative.
1. Structured data: the retail database
A question asked after the tables were modeled:
You What was revenue for the sushi roll flash drive in Pennsylvania in June 2024?
Model → check_answerable(entities=["revenue", "sushi roll flash drive", "Pennsylvania"])
verdict: answerable
revenue covered Order Line › Revenue (order_lines.line_total)
sushi roll flash drive covered_by_value products.product_name = 'USB Food Flash Drive - Sushi Roll'
Pennsylvania covered_by_value states.state_name = 'Pennsylvania'
→ get_query_context(terms=["Order Line", "Product", "State", "Order"])
join path: order_lines → products
order_lines → orders → customers → cities → states
role note: customers.delivery_city_id = cities.city_id is the default;
other roles: customers.billing_city_id (billing)
→ validate_sql(first draft)
✗ Join customers.customer_id = orders.order_id is not how the ontology joins these
tables (orders.customer_id = customers.customer_id).
! No row has states.state_name = 'pennsylvania'. Close real values: 'Pennsylvania'.
→ run_sql(fixed query)
revenue 316.8, order_lines 4
draft Terms used: City, Customer, Order, Order Line, Product, State
Model Revenue was 316.80 from 4 order lines (delivery state Pennsylvania, June 2024).
The Terms behind this answer are still drafts, so treat the definition as unreviewed.
[SQL shown]
And a question the data cannot answer:
You What was return on ad spend for the sushi roll flash drive?
Model → check_answerable(...)
verdict: partially_answerable coverage: 0.33
not covered: "return on ad spend", "ad spend"
Model This database has sales but no advertising spend, so I can't compute ROAS.
I can give you revenue for that product instead.
2. Unstructured data: the store's documents
The demo stores the three documents the way a model would after reading them: each full text as an original note in the vault, the passages worth finding again, the concepts and causal links they describe, and the action items they decide.
You [Merchandising tag guide] Remember this.
Model → save_passages(source={title: "Merchandising tag guide", kind: "wiki", original_text: "# Merchandising tag guide …"},
passages=[{heading: "bestseller", text: "A product the buyers want to feature …", terms: ["Bestseller Tag"]}, …])
original kept in the vault: originals/Merchandising tag guide.md
→ save_concept("Bestseller Tag", relations=[is_a Merchandising Tag], source=2, locator="bestseller")
You What does the bestseller tag mean?
Model → search_text("bestseller")
term Bestseller Tag [Bestseller] Tag
passage Merchandising tag guide › bestseller "A product the buyers want to feature on the home page.
It is a placement decision and does not mean the
product sells the most units."
link: [[originals/Merchandising tag guide#bestseller]]
Model Buyers pick it for home-page placement; it is not a sales ranking
([[originals/Merchandising tag guide#bestseller]]).
You What led to the Midwest sales decline?
Model → run_sql(datasource="knowledge", sql="WITH RECURSIVE chain AS (… FROM term_relations r JOIN chain
ON r.to_term_id = chain.id WHERE r.relation IN ('causes', 'part_of')) …")
Fewer Repeat Orders part_of 1
Delivery Delay causes 2
September Stockout causes 2
Fulfilment Cutover causes 3
You Which product problems sent products back?
Model → run_sql(datasource="knowledge", sql="SELECT t.name, p.heading, snippet(passages_fts, …)
FROM passages_fts JOIN passages p … JOIN passage_terms … JOIN terms t … JOIN term_relations r …
WHERE passages_fts MATCH '"returns"* OR "back"' AND r.relation = 'instance_of'
AND r.to_term = 'Product Quality Issue' ORDER BY bm25(passages_fts)")
Wobbling Chair Base Office Chair …Several chairs came [back] before support …
Ear Cushion Cracking Noise Cancelling Headphones …the product customers sent [back] most often.
The last query is a full-text search, a walk over typed relations and a join to the passages, in one SQL statement that validate_sql checks like any other.
3. Structured and unstructured together
The database says what happened; the documents say why. The model queries both datasources and keeps the numbers and the explanations apart.
You Why did Midwest revenue fall in 2024?
Model → run_sql(datasource="demo", sql="SELECT substr(o.order_date, 6, 2) AS month, … WHERE r.region_name = 'Midwest' …")
month orders_2023 orders_2024
01 32 17
02 30 19
09 34 16
→ search_text("Midwest January September", kinds=["passage"])
Midwest sales review, Q3 2024 › Summary "Midwest orders fell behind last year twice in 2024:
in [January] and February, and again in [September]. …"
→ run_sql(datasource="knowledge", sql="WITH RECURSIVE … term_relations …")
Fulfilment Cutover → Delivery Delay → Fewer Repeat Orders ← September Stockout
Model Midwest orders dropped to 17 and 19 in January and February 2024 (32 and 30 a year earlier) and to 16 in
September (34). The Q3 sales review gives the reasons: fulfilment moved to Lakeshore Logistics on January 8
and deliveries took eight days instead of three, then Lakeshore ran out of desks and chairs in early September
([[originals/Midwest sales review, Q3 2024#January cutover]], [[…#September stockout]]).
You Which open action items deal with that, and who owns them?
Model → run_sql(datasource="knowledge", sql="SELECT a.title, a.owner, a.due_on, group_concat(DISTINCT p.heading)
FROM action_items a JOIN sources s ON s.id = a._source_id JOIN passages p ON p.source_id = s.id
JOIN passages_fts ON passages_fts.passage_id = p.id
WHERE a.status = 'open' AND passages_fts MATCH '"stockout"* OR "delivery"*' GROUP BY …")
Move Ohio and Michigan fulfilment back to Columbus Min Lee 2025-01-31 January cutover, September stockout
Send a win-back offer to Midwest customers Dana Whitfield (none) January cutover, September stockout
You Do bestseller products sell more?
Model → run_sql(datasource="demo", …) bestseller: 7 products, 1,309 units each on average; other: 11 products, 1,365
→ search_text("bestseller") "It is a placement decision and does not mean the product sells the most units."
Model No: bestseller products sold slightly fewer units on average (1,309 against 1,365). That fits the tag guide,
which says buyers assign the tag for home-page placement rather than from sales.
Remove everything the demo added to the knowledge store and the vault with auto-ontology-mcp demo --remove; --no-knowledge sets up only the database.
What it does
| Catalog and profiling | Reads tables, columns, declared keys and comments through SQLAlchemy, then samples rows to record top values, uniqueness, null rates, ranges, date formats and the complete value set of small text enums |
| Key inference | Finds undeclared primary and foreign keys by name and confirms them by value containment. Detects roles (billing_ vs delivery_), self references and bridge tables, and lists identifier columns it could not place |
| Modeling with your model | Hands the model one table at a time with samples, keys and the modeling rules from Auto Ontology, then validates and stores the Terms and column mappings it proposes |
| SQL attributes | Mines reusable filters, measures and join patterns from executed and verified queries and bridge tables, runs each candidate against the database before saving it |
| Answerability | Matches the question's entities to Terms, attributes, SQL attributes and glossary entries, and to names stored in the data (products, places, statuses) |
| Validation | Unknown columns (with the ontology's name for them), joins the ontology does not know, near-miss literals, percent-vs-fraction thresholds, one-to-many fan-out, and a dry run |
| Governance | Draft, certified and deprecated status on Terms and SQL attributes; editing a certified definition returns it to draft. Verified question and SQL pairs feed later answers |
| Knowledge store | A writable store for what your assistant reads: entities with typed columns and references, concepts with a markdown body and typed relations, passages (excerpts and section summaries), and a source with locator and quote behind every record. An index, a change log and an Obsidian export make it usable as an LLM Wiki |
| Full-text search | FTS5 indexes over passages, Term bodies and sources, ranked with bm25. Prefix matching finds words with Korean particles attached (기억 → 기억을), and a substring pass finds them inside compounds. search_text returns snippets, headings and [[links]] to the originals |
| Concept graph in SQL | Terms, typed relations, Term sources and passages are tables next to the records, and the FTS indexes are queryable too, so one validated query can combine a text search, a WITH RECURSIVE walk over relations and the records |
| Originals in Obsidian | The full text of each source stays in your Obsidian vault: a note already there, transcribed text saved as a new note, or a copied PDF or image. The store keeps the summary and excerpts and links back to the heading they came from |
| Portable | Export and import the whole semantic layer as one YAML or JSON document |
Results of the scan on the demo database, as reported by auto-ontology-mcp demo:
| Undeclared foreign keys in the demo | Found and auto-accepted | Proposed for review | Reported as unlinked |
|---|---|---|---|
| 9 | 7 | 1 (employees.manager_id, a self reference) |
1 (orders.sales_rep_id, no name match) |
Quick start
You need uv.
1. Set up the demos (or skip to your own database). This creates the retail database and stores three demo documents in the knowledge store, with their originals in ~/.auto-ontology-mcp/wiki/knowledge/originals:
uvx --from git+https://github.com/gogoymh/auto-ontology-mcp auto-ontology-mcp demo
2. Connect your client. One click for Cursor or VS Code (both need uv installed):
For the other clients, copy the snippet below. auto-ontology-mcp config prints them with absolute paths filled in, which desktop apps need when uvx is not on their PATH.
claude mcp add auto-ontology -- uvx --from git+https://github.com/gogoymh/auto-ontology-mcp auto-ontology-mcp
Codex CLI
Add to ~/.codex/config.toml:
[mcp_servers.auto-ontology]
command = "uvx"
args = ["--from", "git+https://github.com/gogoymh/auto-ontology-mcp", "auto-ontology-mcp"]
OpenClaw
Save the server in OpenClaw's MCP registry (docs). OpenClaw asks for the local Gateway to be stopped while saved servers change, so start it again afterwards:
openclaw mcp set auto-ontology '{"command":"uvx","args":["--from","git+https://github.com/gogoymh/auto-ontology-mcp","auto-ontology-mcp"]}'
openclaw mcp list
The tools show up in OpenClaw's normal coding and messaging tool profiles.
Hermes AgentAdd to ~/.hermes/config.yaml (docs), then run /reload-mcp in a session:
mcp_servers:
auto_ontology:
command: "uvx"
args: ["--from", "git+https://github.com/gogoymh/auto-ontology-mcp", "auto-ontology-mcp"]
Tools are registered as mcp_auto_ontology_<tool>, for example mcp_auto_ontology_run_sql. Hermes passes stdio servers only a baseline environment, so if you keep the store somewhere other than ~/.auto-ontology-mcp, set it under env: (AUTO_ONTOLOGY_HOME: /path).
Add to ~/.cursor/mcp.json:
{
"mcpServers": {
"auto-ontology": {
"command": "uvx",
"args": ["--from", "git+https://github.com/gogoymh/auto-ontology-mcp", "auto-ontology-mcp"]
}
}
}
VS Code
Add to .vscode/mcp.json:
{
"servers": {
"auto-ontology": {
"type": "stdio",
"command": "uvx",
"args": ["--from", "git+https://github.com/gogoymh/auto-ontology-mcp", "auto-ontology-mcp"]
}
}
}
Claude Desktop
Add to claude_desktop_config.json and restart Claude Desktop:
{
"mcpServers": {
"auto-ontology": {
"command": "uvx",
"args": ["--from", "git+https://github.com/gogoymh/auto-ontology-mcp", "auto-ontology-mcp"]
}
}
}
ChatGPT and claude.ai (remote)
Web clients only reach MCP servers on a public HTTPS URL. Run the HTTP transport with a token and expose it through a tunnel:
uvx --from git+https://github.com/gogoymh/auto-ontology-mcp auto-ontology-mcp \
serve --transport http --port 8765 --token "$(openssl rand -hex 24)"
cloudflared tunnel --url http://127.0.0.1:8765 # or: ngrok http 8765
Add https://<tunnel-host>/<token>/mcp as a custom connector with no authentication. Connector settings in these apps take a URL but no custom headers, so the token can sit in the path. Clients that do send headers can use Authorization: Bearer <token> or ?key=<token>. In ChatGPT, write tools need a connector added in developer mode; menu names change between releases.
The search and fetch tools follow the shape ChatGPT expects from connectors used for search and deep research.
3. Ask. Try these in order:
- "Use auto-ontology to build the ontology for the demo datasource: review the proposed joins, model every table, then save useful SQL attributes." (or pick the
build_ontologyprompt) - "Which states brought in the most delivered revenue in 2024?"
- "What was return on ad spend last quarter?" The answer should be that the data cannot say.
- "Why did Midwest revenue fall in 2024?" The numbers come from the database, the reasons from the demo documents.
- Attach a receipt, a statement, an article or your notes on a book and say "Remember this." (or pick the
rememberprompt). See Personal knowledge base. - "Export my knowledge base for Obsidian."
export_wikiwrites pages next to the originals in~/.auto-ontology-mcp/wiki/knowledge; open that folder in Obsidian as a vault.
Connect your database
uvx --from git+https://github.com/gogoymh/auto-ontology-mcp --with "psycopg[binary]" auto-ontology-mcp \
add shop "postgresql+psycopg://readonly_user:pass@host:5432/db" --schemas sales,geo
Add the same --with to the client configuration's args so the server can load the driver.
You can also give the model a URL and let it call add_datasource. Any SQLAlchemy URL works:
| Database | URL | Driver (uvx --with) |
|---|---|---|
| SQLite | sqlite:////absolute/path/file.db |
built in |
| PostgreSQL | postgresql+psycopg://user:pass@host:5432/db |
psycopg[binary] |
| MySQL / MariaDB | mysql+pymysql://user:pass@host:3306/db |
pymysql |
| DuckDB | duckdb:////absolute/path/file.duckdb |
duckdb-engine |
From a clone, uv sync --extra postgres (or mysql, duckdb, all) installs the same drivers.
The test suite runs against SQLite and PostgreSQL 17. MySQL and DuckDB go through the same SQLAlchemy and sqlglot code paths but have not been tested against a live server yet.
Personal knowledge base
Your assistant can already read PDFs, photos, web pages and notes. What it lacks is a place to keep what it read, in a form it can count, compare and cite later. The knowledge store is that place, built from the same parts as Auto Ontology's semantic layer:
| Part | What it holds | Example |
|---|---|---|
| Entity | Facts you will count, sum, filter or compare: a Term with its own table, typed columns and references to other entities | Receipt: paid_on, merchant → Merchant, category, total_krw |
| Concept | Meaning: a one-line definition, a markdown body and typed relations (is_a, part_of, related_to, causes, ...) |
Spaced Repetition is_a Learning Technique, related_to Forgetting Curve |
| Passage | An excerpt or a section summary worth finding again, with its heading or locator in the original and the Terms it is about; full-text indexed | Make It Stick, ch. 2: "Retrieval practice beats rereading." about Retrieval Practice |
| Source | Where each record, concept, relation and passage came from, with a locator and the quote behind it, and the path of the full original in the Obsidian vault | IMG_2041.jpg, line 3; originals/Make It Stick ch2.md |
The store is created the first time the model saves something (datasource knowledge, file ~/.auto-ontology-mcp/knowledge.sqlite). Everything else in this README applies to it as well: synonyms in your own language, the glossary, SQL attributes for definitions like "eating out", validation, certification and verified example queries.
Originals in Obsidian, summaries and excerpts in the store
A long document does not belong in a database row, and an excerpt is no use without the page around it. So the full original stays in your Obsidian vault, where you read and annotate it, and the store keeps what you search and count: a summary on the source, passages for the parts worth finding again, records for the facts and concepts for the ideas. A source points at its original in one of three ways:
| Source field | When | What happens |
|---|---|---|
original |
The note is already in your vault (a web clipping, your own notes) | The path is checked and stored, relative to the vault |
original_text |
The text exists only in the conversation (a web page the model read, a transcript) | Written as a new note in originals/, with the source's title, URI and date as frontmatter |
original_file |
A local PDF, image or document | Copied into originals/. Hidden files and other file types are refused |
Existing files are never overwritten; a different original with the same name gets a numbered name. Passages carry the heading they came from, so search results and exported pages link to [[originals/Note#Heading]] and Obsidian opens the original at that section. Set AUTO_ONTOLOGY_VAULT to your own vault and the generated pages and originals go to its auto-ontology/ folder; otherwise ~/.auto-ontology-mcp/wiki/knowledge is the vault.
Searching and combining
search_text searches passages, Term definitions and bodies, and sources with SQLite FTS5 (bm25 ranking), and returns snippets, headings, the Terms each passage is about and a link to the original. Words match with their Korean particles (기억 finds 기억을) and inside compounds (곡선 finds 망각곡선을).
The same data is in tables next to your records, so SQL can combine them: terms, term_relations (walk chains with WITH RECURSIVE), term_sources, passages, passage_terms, and the indexes passages_fts, terms_fts and sources_fts:
-- Passages that mention 간격 and are about a concept that is_a Learning Technique
SELECT s.title, p.heading, snippet(passages_fts, 3, '[', ']', '…', 12) AS hit
FROM passages_fts
JOIN passages p ON p.id = passages_fts.passage_id
JOIN sources s ON s.id = p.source_id
JOIN passage_terms pt ON pt.passage_id = p.id
JOIN terms t ON t.id = pt.term_id
JOIN term_relations r ON r.from_term_id = t.id
WHERE passages_fts MATCH '"간격"*' AND r.relation = 'is_a' AND r.to_term = 'Learning Technique'
ORDER BY bm25(passages_fts);
validate_sql knows these tables, their joins and the FTS functions, so these queries get the same checks as any other. The Term tables mirror the ontology and are read-only; change Terms through save_concept, define_entity and update_term.
A session
The tool calls and results below are the ones in tests/test_knowledge.py; what the user and the model say is illustrative.
You [two receipt photos] Remember these.
Model → search_ontology("receipt", datasource="knowledge") no match yet
→ define_entity("Merchant", columns=[name (key), city])
→ define_entity("Receipt", table="receipts", synonyms=["영수증"],
columns=[paid_on date required, merchant ref:Merchant, category, total_krw integer])
→ add_records("Receipt", source={"title": "IMG_2041.jpg", "kind": "image"}, records=[
{"paid_on": "2026/10/03", "merchant": "을지로 국수집", "category": "Eating Out",
"total_krw": "12,000원", "_evidence": "합계 12,000"}, ...])
inserted 3 · created Merchant: 을지로 국수집, Chipotle
(dates and amounts normalized; adding the same receipt again changes nothing)
You [book notes] Keep my notes on chapter 2 of Make It Stick.
Model → save_concept("Spaced Repetition", description="Reviewing material at increasing intervals.",
body="...", relations=[is_a Learning Technique, related_to Forgetting Curve],
source={"title": "Make It Stick", "kind": "book"}, locator="ch. 2")
empty concepts created to fill in later: Learning Technique, Forgetting Curve
You How much did I spend eating out in October, and where is that from?
Model → check_answerable(["영수증", "Eating Out"], datasource="knowledge") answerable
→ run_sql("SELECT SUM(total_krw) FROM receipts
WHERE category = 'Eating Out' AND paid_on LIKE '2026-10-%'") 27500
→ get_source(1) IMG_2041.jpg: 3 Receipt and 2 Merchant records
Model 27,500 KRW, from two receipts in IMG_2041.jpg.
If a record in a batch is wrong (a date written as "last tuesday", an amount as "12%", an unknown column), nothing in the batch is saved and every problem is listed, so the model fixes and retries. Records that other records reference cannot be deleted, an entity with records is only dropped with force, and column types cannot change.
Using it as an LLM Wiki
Every part of the LLM Wiki pattern has a counterpart here:
| LLM Wiki | Auto Ontology MCP |
|---|---|
| Raw sources the model reads but never edits | Originals in the Obsidian vault, and sources in the store: title, path or URL, summary and the vault path of the original, linked from every record, concept, relation and passage |
| Wiki pages the model writes | Concepts (definition, markdown body, typed relations) and entities (records with typed columns) |
Schema file (CLAUDE.md, AGENTS.md) |
Rules the server sends with its handshake and tools, plus the remember prompt |
| Ingest | remember → define_entity, add_records, save_concept, save_passages |
| Query | search_text (full text of passages, bodies and sources), search_ontology (Terms, record values), get_term, check_answerable, run_sql |
| Filing good answers back | save_concept(append_body=...), save_example_query |
| Lint | review_ontology prompt; check_readiness counts empty concepts and records without a source |
index.md |
list_terms |
log.md |
recent_changes |
| Obsidian as the viewer | export_wiki: one page per Term (synonyms as aliases, relations as [[links]], its passages), one per source (link to the original, its passages), plus index.md and log.md, next to the originals |
What the structure adds on top of markdown pages:
- Numbers you can trust. "How much on eating out in October?" is a SQL sum over typed records, checked by
validate_sql, not arithmetic over prose. - Provenance per fact. Each record carries its source, locator and quote, so an answer can say which receipt or page it came from.
- No silent duplicates or broken data. Keys and duplicate detection stop the same receipt from being stored twice, and invalid dates, amounts or columns reject the whole batch with reasons.
- Typed links.
is_a,part_of,causesand the like instead of bare backlinks, so "what are the parts of X" has an answer, in SQL if you like. - Search and lookups in one query. A full-text match, a walk over relations and a filter on records can share one SQL statement.
- Review states. Draft, certified and deprecated, as in Auto Ontology, so you know which definitions a person confirmed.
Limits to know about:
- Generated pages are a one-way view. Edits made to them in Obsidian are not read back; change knowledge through the assistant and run
export_wikiagain (it replaces only the pages it generated and never touches originals or pages you wrote). - Search is lexical, not semantic: FTS5 matches words and prefixes, not meaning. Search with the words the sources use, add your own words as synonyms, and translate when the sources are in another language.
- The store indexes passages, not whole originals. What the model did not save as a passage or summary is in the vault but not in
search_text. - It is a single-user store on your machine. Back it up by copying
~/.auto-ontology-mcp.
Large existing tables (bank exports, workout logs) load faster with sqlite-utils into their own database, connected read-only next to the store:
uvx sqlite-utils insert ~/kb/personal.db spending ~/Downloads/card-export.csv --csv
uvx --from git+https://github.com/gogoymh/auto-ontology-mcp auto-ontology-mcp add personal "sqlite:///$HOME/kb/personal.db"
From a chat app, with OpenClaw or Hermes Agent
OpenClaw and Hermes Agent are personal agents you run on your own machine or server and talk to from messaging apps. With auto-ontology-mcp registered (setup), you can send a photo from your phone and ask about it weeks later. The tools are the same; Hermes prefixes them with the server name:
You [photo of a receipt] lunch today
Agent → mcp_auto_ontology_search_ontology(query="receipt", datasource="knowledge")
→ mcp_auto_ontology_add_records(entity="Receipt", records=[{...}],
source={"title": "chat photo, 2026-11-06", "kind": "image"})
You eating out this month vs last month?
Agent → mcp_auto_ontology_check_answerable(...) → mcp_auto_ontology_run_sql(...)
Reading the photo is up to the agent's model, so use one that accepts images. The server runs where the agent runs: ~/.auto-ontology-mcp is that machine's home directory.
Tools
| Stage | Tools |
|---|---|
| Status | check_readiness |
| Connect and scan | add_datasource, scan_datasource |
| Model | get_modeling_worklist, save_table_terms, update_term, save_glossary |
| Relationships | list_relationships, set_relationship, find_join_path |
| SQL attributes | suggest_sql_attributes, save_sql_attribute |
| Explore | search_ontology, get_term, describe_table, lookup_values |
| Answer | check_answerable, get_query_context, validate_sql, run_sql, save_example_query |
| Knowledge store | define_entity, add_records, edit_records, save_concept, save_passages, search_text, get_source, list_terms, recent_changes, export_wiki |
| Share | export_ontology, import_ontology |
| ChatGPT connectors | search, fetch |
Prompts: build_ontology, answer_question, remember, review_ontology. Resource: ontology://guide. The server also sends its workflow as MCP instructions at handshake, so the model knows to check answerability and validate before it runs anything.
Read tools carry readOnlyHint, and tools that can delete carry destructiveHint, so clients can skip or require confirmation accordingly.
How it works
flowchart LR
U([You]) <--> M["Your MCP client's model<br/>Claude Code, Codex, OpenClaw,<br/>Hermes, Cursor, ...<br/>names Terms, reviews joins,<br/>extracts entities, writes SQL"]
M <-- "MCP<br/>stdio or HTTP" --> S["auto-ontology-mcp<br/>scans, profiles, infers keys,<br/>validates, stores, runs read-only"]
S <--> O[("ontology.db<br/>Terms, relationships,<br/>SQL attributes")]
S <-- "validated writes" --> K[("knowledge.sqlite<br/>records, sources, passages,<br/>Term mirror, FTS5 indexes")]
S -- "originals and pages" --> V[("Obsidian vault")]
S -- "read-only" --> D[("Your databases")]
Each step of Auto Ontology maps to one side of that line:
| Step | NVIDIA Auto Ontology | auto-ontology-mcp |
|---|---|---|
| Schema extraction and profiling | Ingestion service | Server: scan_datasource |
| Undeclared foreign keys | LLM picks candidate columns, vector search finds the target key, a SQL probe checks values | Server: name rules plus the same value probe; ambiguous cases go to the model |
| Terms and column mapping | LLM, one table at a time | The model, with the same rules, via get_modeling_worklist and save_table_terms |
| SQL attribute suggestions | Expressions mined from the warehouse's query history, judged by an LLM | Server mines the queries run through it and the verified examples (not the warehouse's own history), the model judges, the server test-runs before saving |
| Joins seen in query history | Recorded from the warehouse's query logs | Joins used by executed and verified queries that are not relationships yet, listed by list_relationships for review |
| Answerability | LLM entity extraction plus embedding search | The model extracts entities; the server matches them lexically and against stored values |
| Writing SQL | LLM agent | The model |
| Checking SQL | Database probes plus an LLM review | Server: database probes and ontology checks in validate_sql |
| Storage | Postgres and pgvector | Local SQLite files |
| Ingesting knowledge | Ingestion service reads databases | The model reads files, images and notes; the server validates and stores the records, concepts and sources it extracts |
Search is lexical because there is no embedding model: token overlap, trigram similarity and phrase match for Terms and columns, and SQLite FTS5 with bm25 for the text of passages, bodies and sources. The server instructions ask the model to search with the schema's vocabulary, translate non-English phrases, and add the user's own words as synonyms with update_term(add_synonyms=[...]), after which they match directly.
Not carried over from Auto Ontology: the web UI and REST API, users, groups and RBAC, zones (access scoping), tags and rules, PII detection, JSONB path checks, embedding search and reranking, chart generation, KumoRFM predictions and Apache Ossie interchange (export and import use this project's own YAML format). Added here and not in Auto Ontology: write access over MCP, the personal knowledge store (Auto Ontology ingests databases, not documents), concept pages with typed relations, provenance on records, passages with full-text search, originals kept in Obsidian, and the Obsidian export.
Safety
The server writes in exactly one place: ~/.auto-ontology-mcp/knowledge.sqlite, through the knowledge tools, which build parameterized statements from validated input. run_sql cannot write there either. Connected databases are only ever read, enforced in three layers:
- SQL guard. One
SELECT,WITHor set operation per call. DML, DDL,PRAGMA,ATTACH,SELECT … INTO,FOR UPDATEand functions that reach files, other servers or server state (pg_read_file,dblink,LOAD_FILE,set_config,load_extension, ...) are refused. Row sources are allow-listed against the scanned catalog, which also blockssqlite_master, DuckDB'sFROM '/path/to/file'andOPENQUERY. - Read-only connections. SQLite opens with
mode=ro, PostgreSQL withdefault_transaction_read_only, DuckDB withread_only, MySQL with a read-only session. - Rollback. Every transaction is rolled back.
Queries time out after 30 seconds and return at most 500 rows (100 by default).
Over HTTP, a token is required to reach the server from anywhere but localhost: without one, requests addressed to any other host are refused as DNS-rebinding protection, which also means a tunnel will not work until you set --token.
[!WARNING]
Datasource URLs, passwords included, are stored in plain text in~/.auto-ontology-mcp/ontology.db. Tool output masks them. Use a read-only database account, and on shared machines setAUTO_ONTOLOGY_ALLOW_DATASOURCE_TOOLS=0so datasources can only be added from the CLI.
Configuration
Environment variables| Variable | Default | Meaning |
|---|---|---|
AUTO_ONTOLOGY_HOME |
~/.auto-ontology-mcp |
Where the ontology store, the knowledge store (knowledge.sqlite) and the demo database live |
AUTO_ONTOLOGY_STORE |
$AUTO_ONTOLOGY_HOME/ontology.db |
Ontology store file |
AUTO_ONTOLOGY_VAULT |
unset | Your Obsidian vault. Originals and generated pages go to its auto-ontology/ folder, and original paths are relative to it. Unset: $AUTO_ONTOLOGY_HOME/wiki/knowledge is the vault |
AUTO_ONTOLOGY_MAX_ROWS |
500 |
Upper bound on rows run_sql returns |
AUTO_ONTOLOGY_QUERY_TIMEOUT_S |
30 |
Per-query timeout in seconds |
AUTO_ONTOLOGY_PROFILE_ROWS |
1000 |
Rows sampled per table while profiling |
AUTO_ONTOLOGY_ALLOW_DATASOURCE_TOOLS |
1 |
0 hides add_datasource and scan_datasource from MCP |
AUTO_ONTOLOGY_MCP_TOKEN |
unset | Token required by the HTTP transport (same as --token) |
AUTO_ONTOLOGY_MCP_HOST / _PORT |
127.0.0.1 / 8765 |
HTTP bind address |
auto-ontology-mcp stdio MCP server (what desktop clients launch)
auto-ontology-mcp serve --transport http --port 8765 --token ...
auto-ontology-mcp demo the retail database 'demo' and three demo documents in the knowledge store
auto-ontology-mcp demo --remove remove the demo database registration and everything the demo documents added
auto-ontology-mcp add NAME URL register and scan a datasource
auto-ontology-mcp scan NAME re-scan; reviewed joins and Terms are kept
auto-ontology-mcp list readiness of every datasource
auto-ontology-mcp export NAME -o ontology.yaml
auto-ontology-mcp export-wiki -o ~/vault/ontology Obsidian vault of the knowledge store
auto-ontology-mcp import NAME ontology.yaml
auto-ontology-mcp remove NAME
auto-ontology-mcp config print client configuration snippets
Development
git clone https://github.com/gogoymh/auto-ontology-mcp && cd auto-ontology-mcp
uv sync
uv run pytest
The PostgreSQL tests run when AO_TEST_PG_URL points at a disposable database:
docker run -d --rm --name aomcp-pg -e POSTGRES_PASSWORD=pw -p 55432:5432 postgres:17
AO_TEST_PG_URL=postgresql+psycopg://postgres:[email protected]:55432/postgres uv run --extra postgres pytest
| Module | Responsibility |
|---|---|
catalog.py, profiling.py, inference.py |
Schema extraction, profiling, key and bridge-table inference |
knowledge.py |
The writable knowledge store: entities, records, concepts, relations, sources |
textindex.py |
Passages, the SQL mirror of Terms and relations, FTS5 indexes and search_text |
vault.py |
Originals in the Obsidian vault: linking, writing and copying them |
wiki.py |
Obsidian vault export |
demo.py, demo_knowledge.py |
The demo database and the demo documents |
service.py |
Everything behind the tools |
validation.py, sqlcheck.py |
Read-only guard, column lineage, probe-based validation |
search.py, graph.py |
Lexical search, join paths |
guidance.py |
Rules handed to the model, adapted from Auto Ontology's prompts |
server.py, http_auth.py, __main__.py |
MCP surface, HTTP token, CLI |
Support the project
If Auto Ontology MCP saved you from a wrong number or replaced a pile of notes, a star on GitHub is the simplest way to help: it is how other people find the project. Bug reports and ideas are welcome in Issues.
Acknowledgements
Built on the ideas and code of NVIDIA Auto Ontology (Apache-2.0): its modeling, relationship, SQL attribute, entity extraction and SQL rules, column profiling, expression mining and database probes. NOTICE lists what came from which file.
Uses FastMCP, SQLAlchemy and sqlglot.
Disclaimer
This is a personal open-source project. I built it on my own time and with my own resources, as a contribution to the open-source community.
- It is not affiliated with, sponsored by or endorsed by NVIDIA, or by any company or organization I am associated with. "Auto Ontology" refers to the NVIDIA project this work builds on.
- It is provided "as is", without warranty of any kind, under the Apache License 2.0. Use it at your own risk.
- You decide what it touches. Review which databases and files you connect, and keep in mind that what the server returns is passed to the model behind your MCP client and handled under that provider's terms.
License
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found
