bizdata-mcp
Connects to a SQLite database containing business data, supporting schema discovery, business-specific reporting tools, and read-only SELECT queries with safety guardrails.
Click on "Deploy 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., "@bizdata-mcp which products should I reorder this week?"
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.
mcp-business-data
An MCP server that lets Claude answer questions about a small business's own data, plus a short command-line agent that does the same thing through the Claude API.
The data here belongs to a made-up outdoor gear shop, Harbor & Pine Supply: about 3,000 customers, 46 products, 4,300 orders and 330 refunds over 18 months (April 2025 to September 2026), generated with seasonality, category growth, a few loyal repeat buyers, one tent with a zipper problem, and some items about to run out. I built it to show the pattern I use with clients: give the model a few well-defined business tools, a read-only SQL escape hatch with real guard rails, and a log of everything it ran.
Swap the SQLite file for your own database and most of this carries over. See Adapting it to your own database.
Quick start
Needs Python 3.11 or newer.
git clone <this repo> mcp-business-data
cd mcp-business-data
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python scripts/make_sample_db.py # writes data/harbor_pine.db (seeded, same data every run)
pytestThen connect it to Claude Desktop or Claude Code (below) and ask something like "which products should I reorder this week?"
Related MCP server: text-to-sql-mcp
The tools
Tool | What it answers |
| Orders, units, revenue, refunds and net revenue by day, week, month, quarter, category, product or channel, with a totals row. |
| Best customers by net revenue after refunds. |
| Refunded dollars as a share of revenue, by category, product, month or channel. |
| Active products that are out of stock, at or below their reorder point, or under N days of cover at the last 30 days' sales rate. |
| Tables, columns, row counts, foreign keys, and notes such as "revenue lives on order_items, not orders". |
| One read-only SELECT for anything the other tools don't cover. Returns |
The schema is also published as an MCP resource, schema://tables, in Markdown.
The business tools exist because "revenue" is ambiguous. Does it include shipping? Cancelled orders? Are refunds subtracted, and by order date or refund date? Left to write its own SQL, a model will pick an answer, and it won't always pick the same one. The fixed tools encode the store's definitions once (completed orders only, shipping excluded, refunds attributed to the original order's date), and the schema notes repeat them for when the model does fall back to run_sql. The business tools only run SQL written ahead of time. Values go in as parameters, and group_by picks from a fixed dict of SQL expressions rather than being pasted into the query.
Every call, from the server or from ask.py, is appended to logs/audit.jsonl:
{"ts": "2026-10-06T15:22:57.245+00:00", "source": "mcp", "tool": "sales_summary", "args": {"start": "2026-04-01", "end": "2026-06-30", "group_by": "category"}, "rows": 6, "duration_ms": 8.06, "ok": true}
{"ts": "2026-10-06T15:22:57.358+00:00", "source": "mcp", "tool": "run_sql", "args": {"query": "DROP TABLE orders"}, "rows": null, "duration_ms": 0.03, "ok": false, "error": "QueryError: only SELECT (or WITH ... SELECT) queries are allowed"}How the read-only guard works
run_sql runs SQL that a model wrote, so it goes through three independent layers. Any one of them would stop a write on its own.
Text check (
src/bizdata/sqlguard.py). Comments are stripped and string literals blanked, then the query must be a single statement starting withSELECTorWITH, with noINSERT,UPDATE,DELETE,DROP,PRAGMA,ATTACHand so on outside of quotes. This is the weakest layer. Its job is to give the model a clear error message it can act on.Read-only connection. The file is opened with SQLite's
mode=roURI flag andPRAGMA query_only = ON. SQLite itself refuses to write.SQLite authorizer. SQLite calls back for every operation in the compiled statement, and the callback allows only
SELECT, column reads, recursive CTEs, and function calls not on a short block list (load_extension,zeroblob,randombloband a few others). That covers anything the text check might have misparsed, because it judges what SQLite actually compiled rather than what the text looked like.
On top of that:
Results stop at 200 rows (
BIZDATA_MAX_ROWS). The response says"truncated": trueso the model knows it's looking at part of the answer.A progress handler cancels a query after 5 seconds (
BIZDATA_TIMEOUT_S). The test suite checks this with an endless recursive CTE.Strings and blobs are capped at 1 MB, and attaching other databases is turned off.
Columns listed in
BIZDATA_HIDDEN_COLUMNS(defaultcustomers.email) read as NULL throughrun_sql, including inWHEREclauses, soWHERE email LIKE 'a%'can't be used to guess them.describe_schematells the model the column is hidden.
The tests in tests/test_run_sql.py call the executor directly with the text check skipped, to show layers 2 and 3 hold by themselves.
What it doesn't protect against
Reading. Anything not hidden can be read, by design. If a table shouldn't be visible to the model, it shouldn't be in this database file, or its columns should be hidden.
Where the data goes. Rows that tools return are sent to whichever model is using them. With Claude Desktop, Claude Code or
ask.py, that means Anthropic's API. Check that this is acceptable for your data before connecting it.Prompt injection through data. If a customer types instructions into a free-text field and a tool returns that text, the model reads it. With read-only tools the damage is limited to a misleading answer, but it's not zero.
Load. A 5-second query can still be an expensive one. On a busy production database, point this at a replica.
The HTTP transport has no authentication. It binds to 127.0.0.1 by default. Don't expose it to a network without putting authentication in front of it.
Sample: "Which product category grew fastest last quarter?"
I haven't run ask.py against the API for this README, so there is no model transcript here. What follows is real output from a local run: the server started over stdio, and these are the tool calls a model would make for this question (output trimmed to the fields that matter).
>>> sales_summary {"start": "2026-04-01", "end": "2026-06-30", "group_by": "category"}
>>> sales_summary {"start": "2026-07-01", "end": "2026-09-30", "group_by": "category"}
category Q2 net Q3 net change
Camping 64,706.55 76,523.35 +18.3%
Water 41,345.00 48,856.00 +18.2%
Hiking 35,804.25 35,682.05 -0.3%
Apparel 16,906.20 19,337.60 +14.4%
Climbing 16,301.05 14,960.30 -8.2%
Accessories 8,666.65 9,309.00 +7.4%Quarter over quarter, Camping edges out Water. That's mostly the season, though. Run the same call for Q3 2025 and the picture changes: Climbing is up 49% on the same quarter last year, Camping 38%, Accessories 35%, and Water is flat. A good answer mentions both, and the system prompt in ask.py tells the model to state which dates it compared.
A few more calls from the same run:
>>> inventory_alerts {}
{"sku": "HIK-008", "name": "Trail Map Case", "stock_on_hand": 0, "reorder_point": 5, "units_last_30d": 9, "days_of_cover": 0.0, "status": "out of stock"}
{"sku": "ACC-003", "name": "Multi-Tool", "stock_on_hand": 1, "reorder_point": 6, "units_last_30d": 11, "days_of_cover": 2.7, "status": "at or below reorder point"}
... as_of 2026-09-30
>>> refund_rate {"by": "product", "start": "2026-01-01", "end": "2026-09-30"}
{"product": "Cedar Flat 4P Family Tent", "units_sold": 138, "units_refunded": 25, "revenue": 47080.1, "refunded": 8602.85, "refund_rate_pct": 18.27}
{"product": "Sun Hoodie", "units_sold": 24, "units_refunded": 4, "revenue": 1269.0, "refunded": 216.0, "refund_rate_pct": 17.02}
...
>>> run_sql {"query": "SELECT email FROM customers LIMIT 2"}
{"columns": ["email"], "rows": [[null], [null]], "row_count": 2, "truncated": false}
>>> run_sql {"query": "DROP TABLE orders"}
Error executing tool run_sql: only SELECT (or WITH ... SELECT) queries are allowedThe family tent is the planted quality problem. The hoodie is noise from 24 units, which is why units_sold sits next to every rate.
Connecting it to Claude
The server runs over stdio by default. Use absolute paths, since neither client starts it from this folder.
Claude Code
claude mcp add --env BIZDATA_DB=/path/to/mcp-business-data/data/harbor_pine.db --transport stdio harbor-pine-data \
-- /path/to/mcp-business-data/.venv/bin/bizdata-mcpOn Windows the command is C:\path\to\mcp-business-data\.venv\Scripts\bizdata-mcp.exe. Add --scope project to write it to a .mcp.json you can commit for your team, or --scope user to have it in every project.
Claude Desktop
Add this to claude_desktop_config.json (Settings > Developer > Edit Config), then restart the app.
{
"mcpServers": {
"harbor-pine-data": {
"command": "/path/to/mcp-business-data/.venv/bin/bizdata-mcp",
"env": {
"BIZDATA_DB": "/path/to/mcp-business-data/data/harbor_pine.db"
}
}
}
}Over HTTP
Streamable HTTP, for a server that other machines reach through an authenticating proxy:
bizdata-mcp --http --host 127.0.0.1 --port 8000
claude mcp add --transport http harbor-pine-data http://127.0.0.1:8000/mcpSettings
All optional, as environment variables:
Variable | Default |
|
|
|
|
|
|
|
|
|
|
ask.py
A command-line agent for when you want answers without a chat app, or want to see the tool-use loop itself.
export ANTHROPIC_API_KEY=sk-ant-... # or put it in .env (see .env.example)
python ask.py "Which product category grew fastest last quarter?"It calls the same Store methods the server wraps, in-process, with no MCP in between. The tool names, descriptions and input schemas are read from the MCP server definition, so there's one place to edit them, and a test fails if a tool and its method ever disagree. The loop is run_agent in src/bizdata/agent.py, about 60 lines: send the question with the tools, run whatever tools come back, return all the results in one message, repeat until the model answers or 12 rounds pass. A bad tool call goes back to the model as an error so it can correct itself.
It uses claude-opus-5-5 at medium effort. Change them with --model, BIZDATA_MODEL or BIZDATA_EFFORT (set BIZDATA_EFFORT= empty for older models that don't accept the effort setting). For current models it also turns on the API's server-side refusal fallback, so a request the main model declines is retried on a fallback model instead of coming back empty. Without a key it prints setup instructions and exits.
Adapting it to your own database
The SQLite-specific parts are small: Store.connect and Store._execute_guarded (connection, authorizer, timeout), describe_schema (uses PRAGMA table_info), the date functions inside reports.py, and the DDL in schema.py. Rewrite TABLE_NOTES, COLUMN_NOTES and CONVENTIONS in schema.py for your tables. That file has more effect on answer quality than any of the code.
For a server database, don't rely on the text check. Let the database enforce read-only access.
PostgreSQL
Connect as a role that only has
SELECTon the tables you want exposed. For hidden columns, use column-level grants (GRANT SELECT (id, name, state) ON customers TO ai_reader) or a view.Set the limits on the role, so they hold however the connection is opened:
ALTER ROLE ai_reader SET default_transaction_read_only = on; ALTER ROLE ai_reader SET statement_timeout = '5s';With psycopg 3, also set
conn.read_only = True, and usecursor.fetchmany(max_rows + 1)for the row cap, as here.describe_schemareadsinformation_schema.columns. In the reports, replacestrftime('%Y-%m', ...)withto_char(date_trunc('month', ...), 'YYYY-MM'), and the:nameparameters with%(name)s.Point it at a read replica if one exists.
MySQL
A user with
SELECTonly, plus column-level grants or views for anything sensitive.Per session:
SET SESSION TRANSACTION READ ONLYandSET SESSION max_execution_time = 5000(milliseconds; it applies to SELECT statements).Parameters become
%(name)swith PyMySQL or mysql-connector, andstrftimebecomesDATE_FORMAT.
Keep the business tools close to the questions the owner actually asks. Five tools that match how they talk about the business beat thirty generic ones.
Limitations
The sample data is generated. The numbers illustrate the tools and say nothing about any real store.
inventory_alertsmeasures sales velocity up to the most recent order in the data rather than today's date, so it works on a static sample. On a live database those are the same thing.One database, one set of credentials, no per-user permissions. Everyone connected to the server sees the same data.
run_sqlreturns at most 200 rows. The model is told when it hit the cap, but it can still reason from a partial result if it ignores the flag.The MCP SDK is pinned to the 1.x line (
mcp>=1.26,<2). Version 2 renamesFastMCPtoMCPServerand moves transport settings ontorun(). I haven't ported to it yet.ask.pyis tested against a fake client and against the real SDK talking to a mocked HTTP endpoint, not against the live API.
Layout
src/bizdata/
server.py MCP server: tool definitions and descriptions, schema resource
store.py read-only connection, authorizer, run_sql, describe_schema, audited methods
sqlguard.py text check for model-written SQL
reports.py fixed SQL for the business tools
schema.py DDL plus the notes the model reads
audit.py JSONL audit log
agent.py tool-use loop used by ask.py
scripts/make_sample_db.py
ask.py
tests/MIT licensed.
Available Tools
6 toolsdescribe_schemaA
List every table with its columns, row counts, foreign keys, and notes on how to use them.
Read the conventions section before writing SQL: it defines revenue, refunds and date formats.
Inputs: no named parameters. Returns: object
inventory_alertsB
Active products that are out of stock, at or below their reorder point, or running low.
Days of cover = stock on hand / average daily units sold over the last 30 days, measured up to the most recent order in the data. Products with no recent sales show no cover figure.
Inputs: min_days_of_cover. Returns: object
refund_rateA
Refunded dollars as a percentage of revenue, plus units sold and refunded, grouped by by.
Dates filter on when the order was placed. Small groups can show extreme rates; check units_sold before drawing conclusions.
Inputs: by, end, start. Returns: object
run_sqlA
Run one read-only SQLite SELECT (or WITH ... SELECT) and return columns, rows and a truncated flag.
Results are capped (200 rows by default) and queries are stopped after a few seconds. Aggregate in SQL rather than pulling raw rows. Call describe_schema first if you haven't seen the tables.
Inputs: query (required). Returns: object
sales_summaryA
Orders, units, revenue, refunds and net revenue, grouped by time period or dimension.
start/end are inclusive YYYY-MM-DD dates; leave either out for no bound. Only completed orders count. Refunds are attributed to the original order's date. Weeks start on Monday. Includes a totals row with average order value.
Inputs: end, start, group_by. Returns: object
top_customersA
The n customers with the highest net revenue (after refunds) in an optional date range.
n is 1-100. Returns customer id, name, state, order count, revenue, refunds, and first/last order time.
Inputs: n, end, start. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
Each analytical tool targets a distinct question (sales over time, top customers, refund rate, inventory health), and describe_schema is clearly a discovery step. The only blur is run_sql, which can technically reproduce any of the specialized tools, but descriptions steer agents toward the purpose-built ones.
All names are snake_case, but the conventions are mixed: run_sql and describe_schema use verb_noun while sales_summary, top_customers, refund_rate and inventory_alerts use noun-style labels. Still readable and predictable enough to navigate.
Six tools is well-scoped for a focused business-analytics server: schema discovery, a SQL escape hatch, and four targeted analytics. Each tool earns its place with no redundancy.
The surface covers revenue, refunds, customers, and inventory, plus describe_schema and a raw SQL escape hatch that fills most gaps. Minor gaps exist for product-level performance or period-over-period comparisons, but agents can work around them via run_sql.
Maintenance
Related MCP Connectors
Your SMB workspace in Claude or ChatGPT: tasks, notes, sales and Michèle's memory, via OAuth.
Connect your ads, shop, analytics, social, CRM and finance platforms once, then let Claude, ChatGPT, Cursor or any MCP client read, join and explain your numbers. Public statistics from the World Bank, IMF, Eurostat, OECD, WHO and SEC filings come as context, searchable and chartable from the same tools. Read-only by design, every number carries its source.
Amazon Seller Central, Ads and Vendor Central in ChatGPT & Claude. 106 tools; writes need approval.
Run your business from Claude: websites, SEO, ads, social, payments, leads and AI media.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants like Claude to query, analyze, and summarize local SQLite e-commerce databases using natural language through 11 tools, schema resources, and pre-built analytical prompts.-
- AlicenseNot gradedqualityCmaintenanceEnables Claude to translate natural-language business questions into safe, read-only SQL queries by exposing tools for listing tables, inspecting schemas, and executing row-capped SELECT statements against PostgreSQL.MIT
- AlicenseNot gradedqualityCmaintenanceEnables Claude or any MCP-capable assistant to explore and query a small business's sales, payments, invoices, and expenses through read-only SQL tools, while personal-data columns and write operations are refused by the database policy.MIT
- AlicenseAqualityCmaintenanceEnables Claude to answer business questions from a SQL database safely, using read-only queries and bounded summaries, while requiring confirmation and audit logging for order-status changes.7MIT