shop-mcp
Enables AI agents to read-only analyze a SQLite e-commerce database, exploring schema and running analytical SQL queries over stdio.
README
shop-mcp
A local, read-only MCP (Model Context Protocol) server that lets an AI agent
analyze a SQLite e-commerce database (shop.db) — customers, products,
orders and order items — over the stdio transport. No HTTP server, no
separate database process: the server opens shop.db directly and exposes
two small, general-purpose tools an agent can use to explore the schema and
run its own analytical SQL.
Built with the official Python MCP SDK (mcp on PyPI).
Project structure
mcp-sql/
├── server.py # the MCP server (stdio transport)
├── shop.db # SQLite database (not modified by this project)
├── requirements.txt
├── .env.example
├── mcp-config.example.json
├── tests/
│ ├── conftest.py
│ ├── test_server.py # unit tests (call tool functions directly)
│ └── test_stdio_integration.py# protocol-level test (spawns server.py over stdio)
└── README.md
Database schema (as actually found in shop.db)
customers(id PK, first_name, last_name, email UNIQUE, phone, created_at)
products(id PK, name, category, price, stock_quantity, created_at)
orders(id PK, customer_id -> customers.id, order_date, status, total_amount)
order_items(id PK, order_id -> orders.id, product_id -> products.id, quantity, unit_price)
orders.status is constrained to: new, processing, shipped,
completed, cancelled. products.category currently has 5 distinct
values. Foreign keys: orders.customer_id → customers.id,
order_items.order_id → orders.id, order_items.product_id → products.id.
The server derives all of this from the live database at query time (via
sqlite_master / PRAGMA table_info / PRAGMA foreign_key_list) — nothing
here is hard-coded, so if shop.db is swapped for another file with a
different schema, get_database_schema will reflect that automatically.
Known data characteristics of the provided shop.db: customers has no
country column, so "customers from Germany" style questions cannot be
answered — the schema tool makes this discoverable, and query_database
returns a clear no such column: country error instead of guessing. All
750 orders currently in the database are dated in 2026 (none in 2025), so a
"revenue in 2025" query correctly returns 0/null, not an error.
Installation
cd mcp-sql
python3 -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
pip install -r requirements.txt
Configuration
The database path is never hard-coded in the source. It is resolved as:
- the
SHOP_DB_PATHenvironment variable, if set; - otherwise
shop.dbnext toserver.py.
Copy .env.example to .env and edit it if you want to point the server at
a different database file (you'll need to load it into your shell/agent
launcher yourself, e.g. export $(cat .env | xargs), or just set
SHOP_DB_PATH directly):
cp .env.example .env
# edit .env, or simply:
export SHOP_DB_PATH=/absolute/path/to/shop.db
Run
source .venv/bin/activate
python server.py
The process speaks MCP over stdio and waits for a client — it will look
"stuck" with no output, which is expected: connect an MCP client (an AI
agent, or mcp-inspector, see below) rather than running it standalone in a
terminal.
Quick manual check with the official MCP Inspector (no install needed):
npx @modelcontextprotocol/inspector --cli .venv/bin/python server.py --method tools/list
Connect to an AI agent
Most MCP-compatible clients (Claude Desktop, Claude Code, etc.) read a JSON
config block like mcp-config.example.json:
{
"mcpServers": {
"shop-mcp": {
"command": "/absolute/path/to/mcp-sql/.venv/bin/python",
"args": ["/absolute/path/to/mcp-sql/server.py"],
"env": {
"SHOP_DB_PATH": "/absolute/path/to/mcp-sql/shop.db"
}
}
}
}
Notes:
- Use the absolute path to the venv's Python interpreter (as above) so
the
mcppackage is found without activating the venv manually; using a barepython3also works ifmcpis installed in whatever environment that resolves to. SHOP_DB_PATHis optional — omit it to use the bundledshop.db.- Absolute paths belong in this configuration file, supplied by whoever
connects the server — never inside
server.pyitself. - Client-specific placement of this block varies (e.g. Claude Desktop uses
claude_desktop_config.jsonwith the samemcpServersshape; other clients may want just the inner{"command": ..., "args": ..., "env": ...}object). Check your client's docs for where the file lives.
Testing
source .venv/bin/activate
python -m pytest tests/ -v
This runs 48 tests, including:
- schema discovery (tables, columns, PK/FK, relationships, row counts);
SELECT,JOIN,WHERE,GROUP BY,ORDER BY, aggregates (COUNT/SUM/AVG/MIN/MAX), subqueries, a safeWITH ... SELECTCTE, and date filtering (strftime);- row-limit clamping and offset-based pagination;
- friendly error handling for invalid SQL, unknown tables/columns, an empty query, and a missing database file;
- read-only safety: every statement type listed in the assignment
(
DELETE,UPDATE,DROP,CREATE,INSERT, plusALTER,REPLACE,TRUNCATE,ATTACH,DETACH,VACUUM,REINDEX, a destructivePRAGMA, a stackedSELECT 1; DROP TABLE ..., and aWITH x AS (...) DELETE ...CTE-disguised delete) is rejected, and the database file's row counts and SHA-256 hash are asserted unchanged afterwards; tests/test_stdio_integration.pylaunchesserver.pyas a real subprocess and drives it through the actual MCP client SDK over stdio (initialize→list_tools→call_tool), rather than calling Python functions directly — this is the same path a real agent uses.
MCP tools
get_database_schema()
No parameters. Call this first whenever you don't already know the exact
table/column names — don't guess them. Returns, per table: row_count,
columns (name, SQLite type, not_null, default_value,
is_primary_key), primary_key, foreign_keys (column, referenced
table/column, ON DELETE/ON UPDATE), and a few sample_rows so the agent
can see real date formats, status values, price magnitudes, etc. A
top-level relationships list gives table.column -> other_table.column
strings derived from the live foreign keys.
query_database(sql, limit=100, offset=0)
Runs one read-only SQL statement (SELECT, or WITH ... SELECT) and
returns {columns, rows, row_count, limit, offset, truncated, total_matching_rows}. Supports JOIN, WHERE, GROUP BY, ORDER BY,
aggregate functions, subqueries, and CTEs. limit is clamped to 1..500
(default 100); use offset to page through larger results.
total_matching_rows and truncated tell the caller whether the current
page is the whole result or there is more to fetch. Errors (bad syntax,
unknown table/column, or a rejected write attempt) are raised as a short,
specific message — never a raw Python traceback.
Security: how read-only is enforced
The assignment explicitly asks not to rely on a single regex/keyword check,
so this server layers four independent defenses — verified in
tests/test_server.py:
- OS-level read-only file handle. The SQLite file is opened with the
URI
file:<path>?mode=ro. SQLite itself then refuses any write (OperationalError: attempt to write a readonly database) no matter what SQL is executed — this holds even if every check below has a bug. PRAGMA query_only = ONis set on every connection as a second, independent SQLite-level guard against writes.- A
sqlite3authorizer callback (Connection.set_authorizer) allow-lists only theSELECT/READ/FUNCTION/RECURSIVEactions at the SQLite engine level and denies everything else —INSERT,UPDATE,DELETE,DROP,ALTER,CREATE,REPLACE,TRUNCATE,ATTACH,DETACH,VACUUM,REINDEX,PRAGMA, transactions, etc. This runs on the parsed statement, so it also catches the classic CTE bypassWITH x AS (SELECT 1) DELETE FROM ...that a naive "must start with SELECT" text check would miss. - Statement-shape checks in
server.py: the submitted text must start withSELECT/WITH(fast, friendly rejection before touching SQLite), and every query is executed wrapped asSELECT * FROM (<query>) LIMIT :limit OFFSET :offset— a single statement is required for this to parse at all, so a stackedSELECT 1; DROP TABLE customersbecomes a plain SQL syntax error rather than two executed statements.
Because layer 1 (mode=ro) is enforced by SQLite/the OS independently of
this server's own logic, shop.db cannot be modified through this server
even if a bug existed in layers 2-4.
Known limitations
customershas nocountry/location column in the providedshop.db, so questions like "customers from Germany" cannot be answered from this data — the schema tool surfaces this rather than the server inventing a column.- All orders in the provided data are dated in 2026; a 2025 revenue query correctly returns 0 rather than an error.
total_matching_rowsinquery_databaseis computed with a secondCOUNT(*)wrapping the same query; for very expensive queries this roughly doubles the work. Given the size of this database (hundreds to a few thousand rows per table) this is not a practical concern.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。