Read-only MCP for the SQL database behind an ERP: provably read-only guard, public benchmark.
Copy the AI prompt to install this server into Claude Code, Cursor, or another agent β or use 1-click editor setup below.
We haven't yet run this listing's install command through our automated sandbox check. This isn't a red flag β we're steadily working through the catalog.
π‘ Paste the JSON block into your client's configuration file under mcpServers, then restart the application.
The numbers for Monday's meeting are already sitting in your ERP's database. So who's still writing the report?
A read-only, self-verifying data layer for the SQL database behind your ERP β weekly reports and a guarded MCP server for AI agents. Every query provably read-only: measured against 28 attacks, not promised in prose.
πΉπ· TΓΌrkΓ§esi: README.tr.md
One scheduled run executes 9 audited SELECT statements and delivers a self-contained HTML report: four KPIs against an 8-week baseline, findings with named drivers, a data-quality gate, and row counts reconciled against the source. No BI license, no agent installed on the ERP server, and no writes β enforced in four layers (lexical, parse-tree, a side-effecting-function guard, and a read-only session), not promised in prose.

This exact report was produced by one command against the bundled demo database β including the three data-quality problems deliberately seeded into it, all caught by the gate.
The same guarded engine talks to AI agents, too. erp-report-engine mcp exposes the ERP to an agent through canonical entities (orders, never LG_001_01_ORFICHE) behind the same read-only guard β the layer the MCP ecosystem keeps getting wrong. The reference PostgreSQL MCP server's read-only mode was walked out of with COMMIT; DROP SCHEMA public CASCADE; and archived; Supabase's MCP became the textbook "lethal trifecta". Here the guard checks the functions a statement calls, not just its shape β and you don't have to take the adjective's word for it:
βΆ Run the 28-attack trust benchmark Β· break the guard yourself, in your browser (the real guard.py, via Pyodide, nothing sent anywhere).
The exact guard.py the tests run, executing in the browser β a file read and the read-only-transaction escape refused, a real aggregate allowed. Paste your own attack β
Every command is also available as python -m erp_report_engine β¦. Add a database driver with the extras: pipx install "erp-report-engine[mssql]" (Logo Tiger / Netsis / Mikro β SQL Server) or [postgres].
Prefer Docker? docker build -t erp-report-engine . then docker run --rm -v "$PWD:/work" erp-report-engine run -c config.demo.yaml. SQLite and PostgreSQL work out of the box; for MSSQL add Microsoft's ODBC driver (see the Dockerfile). Publishing to PyPI is a GitHub Release away β publish.yml builds and uploads via Trusted Publishing (OIDC, no stored token), and CI already builds the wheel and asserts every bundled profile ships.
Open reports/erp_report_<week>.html. You'll see the engine catch a revenue spike and attribute it to one region, flag a two-point on-time decline, list items below two weeks of stock cover β and confess every duplicate and negative row it found on the way.
βΆ See a live sample report: gulmezeren2-byte.github.io/erp-report-engine (also committed at docs/sample-report.html).
Or run run --dashboard for the premium Command Center β a dark, modern, self-contained dashboard with animated KPIs and glowing SPC control-band charts (live):

| Section | What it answers |
|---|---|
| KPI cards | Revenue, orders, on-time %, low-stock count β each vs last week and vs an 8-week baseline |
| Findings | "Revenue +6.5% week-over-week β main driver: region 'Ege' (54% of the week's movement, partly offset by Marmara (β14,697))" β driver named, the segment pulling the other way named too, action suggested |
| Signals (SPC) | "Revenue signal: 148,291 is ABOVE the control limits (UCL 143,078 = mean 93,168 Β± 2.66 Γ avg moving range 18,763, baseline n=25 weeks)" β a genuine shift, separated from week-to-week noise, with the arithmetic and its sample size shown |
| Trends | 13 full weeks of revenue and on-time %, inline SVG (no external assets) |
| Stock attention list | Items below the cover threshold, worst first |
| Data-quality gate | Duplicate IDs, unparseable dates, negative totals, ship-before-order rows |
| Source reconciliation | Rows fetched vs an independent COUNT(*) of the same query β β or MISMATCH |
| SQL audit trail | Every statement executed, with parameters, row counts and timings |
| Run-state memory | "Revenue has declined 3 consecutive weeks" β context beyond the lookback window |
The layer that makes this portable is the semantic profile: a versioned YAML contract that maps one ERP's cryptic schema to three canonical entities β orders, order_lines, inventory β plus an optional receivables entity (open AR, for aging) that a profile maps when the ledger is reachable and everything downstream skips gracefully when it isn't. The engine only ever sees canonical columns. Swap the profile, keep the report.
Pointing software at a production ERP database is a trust decision. This engine treats it that way β the guarantees are enforced in code and covered by tests:
Read-only is enforced in four layers, so no single mistake makes the engine capable of writing:
| Layer | Enforced by |
|---|---|
| Lexical guard | Single statement, SELECT/WITH head, no comments (--, /*, #), no write/DDL keyword, no write-escalating lock hint (TABLOCKX, UPDLOCK, XLOCK). Scanned with string literals blanked, so SELECT 'please delete this note' is a read, not a threat |
| Parse-tree guard | sqlglot parses the statement β and must succeed, or the query is refused: a guard that can't read a query can't vouch for it. It must be a single read query whose AST holds no INSERT/UPDATE/DELETE/CREATE/DROP/ALTER/MERGE/EXEC/INTO node (catches writes hidden inside CTEs) |
| Function guard | Plenty of pure-looking SELECTs are not reads. pg_read_file, lo_export (which writes a file), dblink (which dials out), OPENROWSET, LOAD_FILE, load_extension (arbitrary code), query_to_xml (arbitrary SQL), set_config (switches off the session backstop), SLEEP/BENCHMARK (denial of service) β all refused, by AST and lexically, because OPENROWSET is precisely what sqlglot cannot parse |
| Read-only session | PostgreSQL default_transaction_read_only=on, SQLite PRAGMA query_only, MySQL SET SESSION TRANSACTION READ ONLY + max_execution_time, and a per-statement timeout everywhere |
Ad-hoc SQL β the agent path β is stricter still. query and guarded_query run in strict mode, which default-denies every function the guard cannot name: sqlglot's function registry is the allowlist, since it knows the portable analytic functions and nothing that reads a file or opens a socket. All four bundled profiles pass it β they call no unrecognised function at all.
No reviews yet β be the first to share how this listing worked for you.
Showcase your server listing on GitHub or your project documentation. Embed this dynamic SVG badge to highlight official listing status and live engagement.
[](https://allmcps.com/mcp/erp-report-engine)<a href="https://allmcps.com/mcp/erp-report-engine"><img src="https://allmcps.com/api/badge/erp-report-engine?style=directory" alt="ERP Report Engine on AllMCPs" /></a>