OmniData MCP
Provides read-only SQL querying, schema inspection, row counting, and data profiling on DuckDB databases, with safety guardrails like statement allowlisting and row limits.
Generates bar, line, and scatter charts from SQL query results, saving them to disk and returning them as inline images.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@OmniData MCPprofile the sales table and create a chart of monthly revenue"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
OmniData MCP

A unified Model Context Protocol server for intelligent data engineering & analytics.
OmniData MCP lets LLM clients (Claude Desktop, Cursor, or any MCP-compatible client) query, profile, transform, and visualize data using DuckDB and PySpark, without raw data ever entering the model's context. Only metadata, bounded query results, and summarized outputs are ever returned to the LLM; every operation is logged locally for auditability.
Status: all planned phases complete
Phase | Scope | Tools added |
0 | Project scaffolding, MCP server skeleton |
|
1 | DuckDB query/profiling engine |
|
2 | Visualization |
|
3 | PySpark transformation engine |
|
4 | Hardening: audit logging, consistent error handling, docs | DONE |
See CHANGELOG.md for what changed in each phase, including two real
bugs found and fixed during development (an unhandled-exception error
path in Phase 4, and an unreliable Spark cancellation mechanism in
Phase 3) -- documented honestly rather than glossed over.
Related MCP server: DuckDB MCP Server
Quick start
uv sync
cp .env.example .env
uv run python scripts/seed_sample_data.py # creates sample sales/customers tables
uv run omnidata-db-server # sanity check: should hang silently (correct -- it's waiting on stdio)Connecting an MCP client
Claude Desktop, packaged/MSIX installs (most current Windows installs):
Raw claude_desktop_config.json editing does not work reliably on
this install type -- the file is app-managed and gets overwritten.
Use the included manifest.json:
Settings -> Extensions -> Advanced settings -> "Install Unpacked
Extension" -> select this project's root folder. Update
manifest.json's command/args paths first if your uv install or
project location differ from the defaults.
Claude Desktop (classic config), Cursor, or other MCP clients: Edit your client's MCP config directly:
{
"mcpServers": {
"omnidata-db": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/omnidata-mcp", "omnidata-db-server"]
}
}
}Either way, restart the client and ask it to call health_check to
confirm the connection.
Architecture
The original design called for two separate MCP servers -- a lightweight Database server and a heavier PySpark & Analytics server -- communicating with the client independently:
LLM Client (Claude Desktop / Cursor / custom)
|
(MCP Protocol, stdio)
|
+-------------+-------------+
v v
Database MCP Server PySpark & Analytics Server
(DuckDB / PostgreSQL) (PySpark session / Plotly)As built, this was deliberately consolidated into one server
(db_server.py, one process, one manifest.json, one Claude Desktop
extension) rather than split in two:
LLM Client (Claude Desktop / Cursor / custom)
|
(MCP Protocol, stdio)
|
OmniData MCP Server
(DuckDB engine + PySpark engine +
Plotly charts, one process)Rationale: for a single-user local tool, one process means one
install/uninstall cycle in Claude Desktop, one shared config and audit
log, and no need to coordinate two lifecycles for what's still a small
number of tools (8). The internal module boundaries
(connection.py/query_safety.py for DuckDB,
spark_session.py/spark_pipeline.py for Spark, charts.py for
visualization) still mirror the original two-server split logically --
splitting back into separate processes later, if concurrency or
multi-user needs justify it, would mean moving files, not rewriting
them.
Tool reference
Tool | Purpose | Key args |
| Verify the server + DuckDB connection are alive; reports current guardrail config | -- |
| List tables/views available to query | -- |
| Column names/types/nullability for one dataset |
|
| Exact row count for one dataset |
|
| Bounded, read-only SQL (SELECT/WITH/EXPLAIN/DESCRIBE/SHOW only) |
|
| Per-column stats (nulls, min/max, distinct count, quartiles) via DuckDB's |
|
| Bar/line/scatter chart from a query result -- saved to disk + returned inline |
|
| Declarative transformation pipeline (filter/select/withColumn/groupBy_agg/orderBy/distinct/limit) against a DuckDB table, run through Spark |
|
Security & governance model
The pitch for this project was governance-first: an LLM should be able to work with real data without raw rows ever landing in its context. Every tool honors that in practice, not just in the tagline:
Read-only by construction, not by convention.
run_sql_queryandgenerate_chartboth validate every statement against an allowlist (SELECT/WITH/EXPLAIN/DESCRIBE/SHOWonly, single-statement, no DDL/DML keywords anywhere in the text -- including inside comments or subqueries). Verified against 14 attack/edge cases during Phase 1 development.No arbitrary code execution.
execute_pyspark_jobtakes a declarative JSON pipeline from a fixed set of operations, not raw Python or PySpark code toexec(). Every step is validated before it touches Spark.Bounded by default. Every query auto-injects a
LIMITwhen one isn't specified, and results are hard-capped regardless of what's requested (max_row_limit,max_chart_rows,max_spark_input_rows).Timeouts on every long-running path, since neither DuckDB nor PySpark has this built in: a thread +
connection.interrupt()for DuckDB (confirmed via a genuinely slow query that got cancelled at ~10s and left the connection reusable afterward), a wall-clock timeout with best-effort cancellation for Spark (see the honest limitation noted below).Local audit trail. Every tool call is logged to
data/audit.logas JSON lines -- tool name, argument summary, duration, outcome. Query/operation metadata only, never raw dataset rows, consistent with the "raw data stays local" principle -- and the log itself never leaves your machine either.Consistent, structured errors. Every tool returns the same
{"error": "..."}shape on failure (enforced by the@auditeddecorator wrapping all 8 tools), rather than some failing cleanly and others surfacing raw unhandled exceptions.
Design decisions
Area | Decision |
Package manager |
|
MCP framework |
|
Query safety | Statement allowlist ( |
Sampling policy | Auto-inject |
PySpark pipelines | Declarative op allowlist, not arbitrary code execution; no join operation exposed |
PySpark session | Singleton |
Chart rendering | Plotly + |
Chart delivery | Saved to disk as a real file and returned as an inline MCP image -- the file is the reliable path given client display limitations (see below) |
Config |
|
Audit logging | JSON lines to |
Error handling | Every tool returns |
Project layout
omnidata-mcp/
|-- pyproject.toml
|-- manifest.json # Claude Desktop unpacked-extension manifest
|-- LICENSE
|-- CHANGELOG.md
|-- .env.example
|-- scripts/
| `-- seed_sample_data.py # creates sample sales/customers tables
|-- src/omnidata_mcp/
| |-- config.py # centralized settings (pydantic-settings)
| |-- connection.py # DuckDB connection + timeout enforcement
| |-- query_safety.py # read-only SQL allowlist validator
| |-- charts.py # Plotly chart building
| |-- spark_session.py # lazy SparkSession + timeout handling
| |-- spark_pipeline.py # declarative pipeline op validator/executor
| |-- audit.py # @audited: logging + error normalization
| `-- db_server.py # the 8 MCP tools
`-- data/ # local DuckDB file, charts/, audit.log (gitignored)Trying it out
Once uv sync and the seed script have run, ask your MCP client
things like:
"What datasets are available?" ->
list_datasets"What columns does the sales table have?" ->
get_schema"Profile the sales table" ->
get_data_profile"What's total revenue by product category?" ->
run_sql_query"Chart total revenue by product category" ->
generate_chart(bar)"Chart revenue over time by region as a line chart" ->
generate_chartwithseries_columnset to region"Use PySpark to compute average revenue per order, grouped by region, for orders over $50" ->
execute_pyspark_job(filter + groupBy_agg)
scripts/seed_sample_data.py populates data/omnidata.duckdb with two
tables: sales (500 rows, deliberately includes a few NULLs and one
outlier -- useful for exercising get_data_profile) and customers
(50 rows, referenced by sales.customer_id). Re-run it any time to
reset to a clean sample dataset.
Troubleshooting
PySpark tools fail to start on Windows, mentioning NativeIO$Windows
or UnsatisfiedLinkError. PySpark needs a JVM, and on Windows
specifically it also needs Hadoop's winutils.exe even in local mode
-- a well-known PySpark-on-Windows requirement unrelated to this
project.
Install Java 17 or 21 (JDK) from Eclipse Temurin; set
JAVA_HOMEand add%JAVA_HOME%\binto PATH.Download winutils.exe matching your Spark/Hadoop version from a trusted mirror (e.g. cdarlint/winutils), place it at
<hadoop_home>\bin\winutils.exe,setx HADOOP_HOME "<hadoop_home>", add%HADOOP_HOME%\binto PATH.Open a fresh terminal after either change -- PATH updates via
setxdon't apply retroactively to already-open windows.
Chart generation succeeds (per the tool result / audit log) but
nothing renders inline in the chat. This is a known Claude Desktop
limitation, not a bug in this project: it does not currently render
inline images from locally-installed unpacked extensions (as opposed
to marketplace-published ones). Every chart is always saved to
data/charts/<timestamp>_<id>.png regardless -- the tool's response
text includes that file's full path; open it directly.
Chart rendering fails demanding a Chrome install. Something bumped
kaleido past 1.0. Re-pin it: kaleido==0.2.1 in pyproject.toml,
then uv sync.
execute_pyspark_job keeps timing out on legitimately large inputs.
The timeout (spark_job_timeout_seconds, default 30s) has only
best-effort cancellation -- true in-JVM cancellation via Python threads
was tested during development and found unreliable in extreme cases (see
CHANGELOG.md, Phase 3). Try lowering max_spark_input_rows or adding
an earlier filter/limit step to your pipeline so there's less work
to do in the first place.
Any tool install/sync step fails with "no space left on device" /
WinError 112, even though your project's own .venv is on a drive
with plenty of room. Some Windows install paths can't be redirected
(Windows' native MSIX/AppX package installer always stages to
C:\Program Files\WindowsApps, and some tools' TEMP usage defaults
back to C:\Users\<you>\AppData\Local\Temp unless TEMP/TMP are
permanently redirected via setx). If you've hit this before,
re-check your TEMP/TMP/UV_CACHE_DIR environment variables are
still pointing where you expect, and separately confirm actual free
space on C: -- redirection isn't a substitute for real headroom on
installers that can't be redirected at all.
License
This project is licensed under the MIT License.
Author
MOSTAFA ABDELHAMED | Junior AI & DS Researcher | NVIDIA Gen AI Certified LinkedIn
Maintenance
Related MCP Servers
- Alicense-qualityDmaintenanceAn enhanced Model Context Protocol server that enables LLMs to inspect database schemas with rich metadata and execute read-only SQL queries with safety checks.24225MIT
- Alicense-qualityDmaintenanceA Model Context Protocol server implementation that connects AI assistants to DuckDB, enabling them to query and analyze data from various sources including CSV, Parquet, JSON, and cloud storage through SQL.17MIT
- AlicenseAqualityBmaintenanceA Model Context Protocol server that enables large language models to access database metadata and perform cross-engine data querying across diverse database ecosystems.1652Apache 2.0
- Flicense-qualityBmaintenanceModel Context Protocol (MCP) server that gives AI assistants a safe, correct data-analyst capability over business metrics - without raw SQL improvisation.
Related MCP Connectors
The grounded data layer for any LLM: governed SQL, metrics, lineage and catalog over your data.
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
A Model Context Protocol server for Wix AI tools
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/MostafaAI10/OmniData-MCP'
If you have feedback or need assistance with the MCP directory API, please join our Discord server