OmniData MCP

OmniData MCP

A unified Model Context Protocol server for data engineering and analytics that lets LLM clients query, profile, transform, and visualize data using DuckDB and PySpark, while keeping raw data out of the model's context through read-only, bounded, and audited operations.

Category
访问服务器

README

OmniData MCP

image alt

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.

Registry & Certification

Glama MCP Server

OmniData MCP is officially published and indexed on the Glama MCP Registry, enabling seamless discovery and integration across supported LLM desktop environments.


Status: all planned phases complete

Phase Scope Tools added
0 Project scaffolding, MCP server skeleton health_check
1 DuckDB query/profiling engine list_datasets, get_schema, get_row_count, run_sql_query, get_data_profile
2 Visualization generate_chart
3 PySpark transformation engine execute_pyspark_job
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.

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:

graph TD
    Client["LLM Client<br/>(Claude Desktop / Cursor / custom)"]
    Client -->|MCP Protocol, stdio| DB["Database MCP Server<br/>(DuckDB / PostgreSQL)"]
    Client -->|MCP Protocol, stdio| Spark["PySpark & Analytics Server<br/>(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:

graph TD
    Client["LLM Client<br/>(Claude Desktop / Cursor / custom)"]
    Client -->|MCP Protocol, stdio| Server["OmniData MCP Server"]

    subgraph Server["OmniData MCP Server (one process)"]
        direction TB
        DuckModule["DuckDB engine<br/>connection.py / query_safety.py"]
        SparkModule["PySpark engine<br/>spark_session.py / spark_pipeline.py"]
        ChartModule["Plotly charts<br/>charts.py"]
    end

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
health_check Verify the server + DuckDB connection are alive; reports current guardrail config --
list_datasets List tables/views available to query --
get_schema Column names/types/nullability for one dataset dataset
get_row_count Exact row count for one dataset dataset
run_sql_query Bounded, read-only SQL (SELECT/WITH/EXPLAIN/DESCRIBE/SHOW only) sql, row_limit
get_data_profile Per-column stats (nulls, min/max, distinct count, quartiles) via DuckDB's SUMMARIZE dataset
generate_chart Bar/line/scatter chart from a query result -- saved to disk + returned inline sql, chart_type, x_column, y_column, series_column, title
execute_pyspark_job Declarative transformation pipeline (filter/select/withColumn/groupBy_agg/orderBy/distinct/limit) against a DuckDB table, run through Spark source_dataset, operations, row_limit

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_query and generate_chart both validate every statement against an allowlist (SELECT/WITH/EXPLAIN/DESCRIBE/SHOW only, 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_job takes a declarative JSON pipeline from a fixed set of operations, not raw Python or PySpark code to exec(). Every step is validated before it touches Spark.
  • Bounded by default. Every query auto-injects a LIMIT when 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.log as 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 @audited decorator wrapping all 8 tools), rather than some failing cleanly and others surfacing raw unhandled exceptions.

Design decisions

Area Decision
Package manager uv
MCP framework mcp SDK 2.0.0 (MCPServer, the successor to the earlier FastMCP class -- same decorator API)
Query safety Statement allowlist (SELECT/WITH/EXPLAIN/DESCRIBE/SHOW only), single statement per call, query timeout
Sampling policy Auto-inject LIMIT when absent; hard cap on rows returned per call; separate row-count tool
PySpark pipelines Declarative op allowlist, not arbitrary code execution; no join operation exposed
PySpark session Singleton SparkSession, local[*], lazily initialized on first use, log level forced to ERROR to protect the stdio protocol
Chart rendering Plotly + kaleido==0.2.1 pinned (self-contained; kaleido>=1.0 needs a separate Chrome install)
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 pydantic-settings, loaded from .env (see .env.example)
Audit logging JSON lines to data/audit.log; metadata only, never raw rows
Error handling Every tool returns {"error": "..."} on failure, enforced centrally

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_chart with series_column set 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.

  1. Install Java 17 or 21 (JDK) from Eclipse Temurin; set JAVA_HOME and add %JAVA_HOME%\bin to PATH.
  2. 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%\bin to PATH.
  3. Open a fresh terminal after either change -- PATH updates via setx don'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

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选