QueryMesh MCP
A local MCP server that lets an AI-powered IDE query 26+ production databases in plain English — schema-aware NL2SQL for validating data assumptions while building features, without opening SSMS or DataGrip.
The Problem
Warehouse operations run on five different database systems across a dozen distribution centers — a voice-picking system, a WMS execution engine, an internal ops application, an order management platform, and an ETL archive in AWS Athena. Answering a simple operational question, or checking a data assumption while building a feature, meant knowing which of those systems actually held the answer, then writing correct SQL against it.
In practice that meant opening DataGrip or SSMS, remembering which database had what, writing SQL with the right schema prefixes and join patterns, and constantly cross-referencing documentation for table and column names that don't follow a consistent convention across systems. Only developers or analysts with deep SQL knowledge could answer ad-hoc questions — and even they were re-deriving the same schema knowledge over and over.
I also wanted this for my own AI-assisted development workflow: when an AI coding assistant is adding a feature that touches one of these systems, it's far more useful — and far safer — if it can check the actual schema and current data before writing code against it, instead of guessing at column names or trusting stale comments.
Solution Architecture
Operator or developer asks a question in the IDE chat. The AI assistant reads MCP tool descriptions at session start and decides when a database question requires calling the server.
Registers tools for discovery, query execution, schema docs, and Excel export. Silently pulls schema context, proposes a query, executes only after approval, and returns formatted results.
get_schema tool serves markdown docs — table/column names, joins, known business logic
Regex, word-boundary DML/DDL rejection before any query reaches a connection
100-row default in chat, 50K max on Excel export — no accidental full-table dumps
Strips connection strings, passwords, IPs, and file paths from every error response
DB1 (picking/loading, 11 DCs) and DB3 (internal ops app — EOD reports, QC, fill rates)
DB2 (WMS execution, 12 DCs + shared schema), DB4 (order management), DB5 (perishables)
ETL archive — historical queries against S3-backed data lake tables
Row-limited results render inline. On request, export_to_excel runs the full query (up to 50K rows) and writes an auto-fit .xlsx file.
What It Looks Like
The interaction happens entirely inside the IDE's chat panel. Schema lookups are invisible to the user — the AI calls get_schema internally, then shows only the proposed query and, after approval, the result. Mockup below is recreated for this write-up; data shown is placeholder.
Proposed query — DB2 (shared, DC-04 schema)
SELECT COUNT(*) FROM DC04.INVENTORY_DETAIL
WHERE status_code = 'HOLD' | count |
|---|
| 0 |
0 pallets on hold at DC-04. Want this exported to Excel?
Key Design Decisions
Python over .NET. This was originally specced as a .NET console app — the natural choice given my day-to-day stack. I switched to Python for zero-compile iteration while shaping the tool surface, a much simpler uvx-based distribution story, and a better fit generally for text/data wrangling than for a compiled console app.
Schema-as-context, not live introspection. Rather than having the AI run INFORMATION_SCHEMA queries at request time, each database system has a comprehensive markdown doc served through the get_schema tool — built from existing SQL query files already in the production codebase, live schema introspection, application enum definitions, and known join patterns. This gives the AI things no schema query can: e.g. "holds live in the inventory detail table with status_code = 'HOLD'" or "fork work types: LD = letdown, PU = putaway, PS = pallet select, PM = pallet move" — business logic that only exists in institutional knowledge otherwise.
Read-only enforced at the protocol layer, not the database layer. Regex-based validation with word-boundary matching rejects every DML/DDL keyword before a query ever reaches a connection — INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, and friends. Keyword matches inside string literals are ignored, so a query filtering on a status value that happens to contain "delete" as a substring doesn't false-positive.
Two connection shapes for the same engine. DB2 runs as its own Oracle schema per warehouse on a shared RDS instance. The server supports both direct per-warehouse connections (no prefix) and a shared cross-schema connection that takes a schema prefix — so a query can target one DC directly or reach across DCs when the question calls for it.
FORBIDDEN = [
"INSERT", "UPDATE", "DELETE", "DROP", "ALTER",
"TRUNCATE", "CREATE", "EXEC", "MERGE", "GRANT",
]
def validate_read_only(sql: str) -> None:
"""Raise if sql contains a write/DDL keyword outside string literals."""
stripped = _strip_string_literals(sql) # avoid false positives on values
for keyword in FORBIDDEN:
pattern = rf"\b{keyword}\b"
if re.search(pattern, stripped, re.IGNORECASE):
raise ReadOnlyViolation(
f"Query rejected — '{keyword}' is not permitted. "
f"This server only supports read (SELECT) queries."
) Technical Stack
| Component | Technology | Purpose |
|---|---|---|
| MCP Server | Python 3.11+ / mcp SDK | JSON-RPC stdio transport, tool registration |
| SQL Server | pyodbc + ODBC Driver 17 | DB1 (picking/loading), DB3 (internal ops app) |
| Oracle | oracledb (thin mode) | DB2 (WMS execution), DB4 (order mgmt), DB5 (perishables) |
| AWS Athena | boto3 | ETL archive data |
| Excel Export | openpyxl | Formatted .xlsx output with auto-fit columns |
| Config | YAML | Connection metadata; real credentials in a gitignored local file |
| Distribution | uvx / pyproject.toml | Zero-install launch from any IDE workspace |
Database Coverage
| System | Engine | Connections | Purpose |
|---|---|---|---|
| DB1 | SQL Server | 11 DCs | Voice-directed picking and loading assignments |
| DB2 | Oracle | 12 DCs + shared | WMS execution — forklift work, inventory, receiving, shipping |
| DB3 | SQL Server | 1 | Internal ops application — EOD reports, QC inspections, audits, fill rates |
| DB4 | Oracle | 1 | Order management — orders, customers, items |
| DB5 | Oracle | 1 | Perishables management |
| Athena | AWS | 1 | ETL archive — historical queries |
MCP Tools Exposed
| Tool | Parameters | Description |
|---|---|---|
list_connections | — | Shows available databases — never exposes credentials |
list_tables | connection_name | Table discovery for a database |
describe_table | connection_name, table_name | Column definitions and types |
query_sql_server | connection_name, sql | Execute read-only SQL (100-row default) |
query_oracle | connection_name, sql | Execute read-only Oracle SQL |
query_athena | sql | Execute read-only Athena/Presto SQL |
export_to_excel | connection_name, sql, filename | Full export up to 50K rows as .xlsx |
get_schema | topic | Returns structured schema documentation |
list_schemas | — | Lists available schema docs |
Security Model
- Read-only enforcement. Regex-based SQL validation with word-boundary matching rejects all DML/DDL keywords. Keywords inside string literals are ignored, so no false positives on legitimate filter values.
- Credential isolation. Connection strings live only in a gitignored local config file.
list_connectionsreturns names and descriptions only — never connection details. - Error sanitization. Regex patterns strip IPs, hostnames, passwords, AWS credentials, and file paths from every error response before it reaches the chat context.
- Row limiting. 100-row default for chat queries, 50K max for Excel exports — prevents accidental full-table dumps into the AI's context window.
Project Structure
querymesh-mcp/
├── pyproject.toml # Package config, uvx entry point
├── config.yaml # Connection metadata (committed)
├── config.local.yaml # Real credentials (gitignored)
├── src/querymesh_mcp/
│ ├── server.py # MCP server entry point
│ ├── config.py # YAML config loading
│ ├── models.py # Data models
│ ├── services/
│ │ ├── validator.py # Read-only SQL enforcement
│ │ ├── limiter.py # Row limit injection/capping
│ │ └── sanitizer.py # Error message sanitization
│ ├── db/
│ │ ├── sqlserver.py # pyodbc execution
│ │ ├── oracle.py # oracledb thin mode execution
│ │ └── athena.py # boto3 Athena execution
│ └── tools/
│ ├── connections.py # Discovery tools
│ ├── query.py # Query execution tools
│ ├── export.py # Excel export tool
│ └── schema.py # Schema documentation tools
└── schemas/
├── db1-schema.md # Picking/loading tables
├── db2-schema.md # 30+ WMS execution tables
├── db3-schema.md # Internal ops app tables + enum reference
└── db4-schema.md # Order management tables Results
| Query turnaround | Seconds — down from minutes of tool-hopping and manual SQL |
| Database connections | 26+ across 4 engines (SQL Server, Oracle, Athena) |
| Accessibility | Non-SQL users can get operational answers directly, in plain English |
| Write risk | Zero — enforced at the protocol layer, before any query reaches a connection |
| Distribution | User-level MCP config — works from any IDE workspace, zero-install via uvx |
My Role
Personal tool, built for my own workflow and shared with the broader ops/dev team. I designed and built the full system end to end.
- Designed the MCP server architecture — stdio JSON-RPC transport, tool registration, connection routing across 4 database engines
- Built the schema-as-context system — catalogued 50+ existing production queries, cross-referenced application enum definitions, and wrote structured markdown schema docs per system
- Implemented read-only SQL validation with word-boundary regex matching and string-literal-aware keyword detection
- Built error sanitization to strip credentials, IPs, and file paths from all error paths before they reach the AI's context
- Implemented row limiting and the Excel export pipeline with openpyxl auto-fit formatting
- Packaged for zero-install distribution via
uvxandpyproject.toml, with user-level MCP config for use across any IDE workspace
Company, system, and database names have been anonymized. Architecture and technical details are accurate.