shop-mcp
# shop-mcp — Read-Only SQLite MCP Server
MCP-сервер на Python, предоставляющий AI-агенту (например, [Pi](https://github.com/badlogic/pi-mono/)) безопасный **read-only** доступ к базе данных SQLite `shop.db` через stdio.
Агент самостоятельно исследует схему БД, пишет SQL-запросы и решает аналитические задачи. Сервер не содержит готовых ответов — только инструменты для исследования и выполнения read-only запросов.
```
AI Agent (Pi)
│ stdio
▼
┌────────────────────┐
│ MCP Server │ list_tables / describe_table / read_query
└─────────┬──────────┘
▼
SQL validation ← только один SELECT / WITH ... SELECT
▼
read-only guard ← connection authorizer
▼
SQLite (mode=ro) ← файл физически невозможно изменить
```
## 1. Requirements
- Python 3.13+
- [uv](https://docs.astral.sh/uv/)
- Файл базы данных `shop.db` (уже находится в корне проекта)
## 2. Installation
```bash
uv sync
```
uv создаст виртуальное окружение и установит зависимости. Вручную создавать venv не нужно.
## 3. Database configuration
Путь к базе не захардкожен и настраивается через переменную окружения.
**Вариант A — переменная окружения (абсолютный путь):**
```bash
export SHOP_DB_PATH=/absolute/path/to/shop.db
export MAX_RESULT_ROWS=1000 # опционально, default 1000
```
**Вариант B — без настройки (fallback):** если `SHOP_DB_PATH` не задана, сервер использует `shop.db` из корня проекта.
Допустимо также скопировать `.env.example` в `.env` и указать значения там (сервер читает `.env` из корня проекта; переменные окружения имеют приоритет):
```bash
cp .env.example .env
```
## 4. Run MCP locally
```bash
uv run python -m shop_mcp.server
```
Сервер работает через stdio и ожидает MCP-протокол на stdin/stdout — отдельно его запускать не нужно, его запускает сам клиент (Pi). Ручной запуск выше полезен только для отладки.
Некорректная конфигурация (например, отсутствует файл БД) завершает процесс с понятным сообщением в stderr.
## 5. Connect MCP to Pi
Pi подключает MCP-серверы через пакет `pi-mcp-adapter` и читает конфигурацию из `.mcp.json` в корне проекта. Такой файл уже входит в репозиторий:
```json
{
"mcpServers": {
"shop": {
"command": "uv",
"args": ["run", "python", "-m", "shop_mcp.server"],
"cwd": "/Users/stalexsm/projects/shop-mcp"
}
}
}
```
Для другой машины поправьте `cwd` на абсолютный путь к каталогу проекта (или замените на `env` с переменной `SHOP_DB_PATH`):
```json
{
"mcpServers": {
"shop": {
"command": "uv",
"args": ["run", "python", "-m", "shop_mcp.server"],
"cwd": "/absolute/path/to/shop-mcp",
"env": {
"SHOP_DB_PATH": "/absolute/path/to/shop.db",
"MAX_RESULT_ROWS": "1000"
}
}
}
}
```
Запускать отдельный HTTP-сервер или вручную держать `python server.py` в терминале не требуется: Pi сам стартует процесс по stdio (лениво, при первом обращении к инструментам).
Если адаптер ещё не установлен:
```bash
pi install npm:pi-mcp-adapter
```
Затем перезапустите Pi в каталоге проекта. Инструменты сервера появятся в панели `/mcp`.
## 6. Available tools
### `list_tables`
Список таблиц БД с кратким описанием и количеством строк. Отправная точка исследования схемы. SQL не требуется.
### `describe_table`
Структура одной таблицы: колонки (`name`, `type`, `nullable`, `primary_key`, `default`) и foreign keys в виде `orders.customer_id -> customers.id`. Несуществующая таблица даёт понятную ошибку со списком доступных таблиц.
### `read_query`
Выполнение одного read-only SQL-запроса (`SELECT` или `WITH ... SELECT`).
Параметры:
- `sql` (обязательный) — текст запроса;
- `max_rows` (опциональный) — запрошенный лимит строк; серверный hard limit `MAX_RESULT_ROWS` (по умолчанию 1000) не может быть превышен.
Поддерживается обычная аналитика SQLite: `JOIN`, `LEFT JOIN`, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT/OFFSET`, `COUNT/SUM/AVG/MIN/MAX`, `DISTINCT`, `CASE`, CTE.
Результат — структурированный JSON:
```json
{
"columns": ["name", "revenue"],
"rows": [["Ноутбук UltraBook 15", 6569270.0]],
"row_count": 1,
"truncated": false,
"execution_time_ms": 0.716
}
```
`truncated: true` означает, что из-за лимита возвращена только часть строк — уточните запрос (`LIMIT`, `WHERE`, агрегация), не считая данные полными.
## 7. Security model
Три независимых уровня защиты:
1. **SQL validation** — разрешён ровно один statement, начинающийся с `SELECT`/`WITH`. Запрещены `INSERT`, `UPDATE`, `DELETE`, `REPLACE INTO`, `DROP`, `ALTER`, `CREATE`, `ATTACH`, `DETACH`, `VACUUM`, `REINDEX`, `PRAGMA` и другие изменяющие операции. Multi-statement запросы (`SELECT ...; DELETE ...`) отклоняются целиком. Валидатор понимает строковые литералы, комментарии и закавыченные идентификаторы, поэтому `'DELETE'` внутри строки не считается нарушением.
2. **Connection authorizer** — всё, что не является чтением (SELECT / чтение таблицы / вызов функции), отклоняется на этапе подготовки запроса.
3. **`mode=ro`** — файл SQLite открывается в read-only режиме; даже при обходе первых двух уровней физическая запись невозможна.
Ошибки возвращаются агенту в понятном виде (`Database query failed: no such column: foo`) — без traceback, путей файловой системы и деталей реализации.
`shop.db` — read-only source of truth: сервер не изменяет ни содержимое, ни структуру файла. Это зафиксировано тестом целостности (checksum + счётчики строк до/после всех попыток разрушающих операций).
## 8. Example questions
Задайте эти вопросы агенту Pi — он сам вызовет `list_tables`, `describe_table` и `read_query`:
- Show me all available tables and explain what information each table contains.
- Who is the customer who spent the most money?
- What are the top 5 best-selling products?
- What are the top 3 product categories by revenue?
- How much revenue did we generate in 2025?
- Which customer placed the most orders?
Справка по бизнес-логике (агент выводит это из описаний инструментов, сервер ответов не кодирует):
- revenue по товарам/категориям считается как `SUM(order_items.quantity * order_items.unit_price)`;
- заказы со статусом `cancelled` не учитываются;
- revenue по годам считается по `orders.order_date`; если заказов нет — корректный ответ `0`.
### Вопрос про страны
**How many customers are from Germany?** — на этот вопрос нельзя достоверно ответить: в таблице `customers` нет поля `country` (только `first_name`, `last_name`, `email`, `phone`, `created_at`). Сервер отдаёт агенту достоверную информацию о схеме, а агент обязан сообщить, что требуемых данных в БД нет, вместо того чтобы выводить страну из email/телефона или гадать.
## 9. Testing
```bash
uv run pytest
```
Набор тестов (66):
- `tests/test_database.py` — read-only подключение, discovery схемы, foreign keys, закрытие соединений;
- `tests/test_security.py` — все запрещённые операции (раздел 24 спецификации), multi-statement, integrity-тест БД;
- `tests/test_tools.py` — интеграционные тесты MCP-инструментов через реальную клиентскую сессию (in-memory transport), включая обработку ошибок;
- `tests/test_analytics.py` — аналитические сценарии (раздел 27) со сверкой против независимого SQLite-источника, лимиты размера результата.
Тесты не изменяют `shop.db` (integrity-тест сверяет checksum файла).
## 10. Troubleshooting
| Симптом | Причина и решение |
|---|---|
| `Configuration error: Database file not found` | `SHOP_DB_PATH` указывает на несуществующий файл. Укажите абсолютный путь или положите `shop.db` в корень проекта. |
| Инструменты не видны в Pi | Убедитесь, что `.mcp.json` лежит в корне проекта, `cwd` указывает на каталог проекта, установлен `pi-mcp-adapter` (`pi install npm:pi-mcp-adapter`), и перезапустите Pi. |
| `Multiple SQL statements are not allowed` | В одном вызове `read_query` допускается ровно один statement; разделите запрос на несколько вызовов. |
| `Only read-only queries are allowed` | Запрос начинается не с `SELECT`/`WITH` либо содержит DML/DDL. Перепишите запрос как SELECT. |
| Результат неполный (`truncated: true`) | Сработал лимит строк. Добавьте `LIMIT`/`WHERE`/агрегацию или увеличивать его не пытайтесь — hard limit задаёт сервер. |
| Хочу другой лимит строк | Задайте `MAX_RESULT_ROWS` в окружении (сервер перезапустится Pi автоматически при следующем старте). |
## Project layout
```
shop-mcp/
├── README.md
├── pyproject.toml
├── uv.lock
├── .env.example
├── .gitignore
├── .mcp.json # конфигурация MCP для Pi
├── shop.db # read-only source of truth
├── scripts/
│ └── smoke_stdio.py # ручной smoke-тест через реальный stdio
├── src/shop_mcp/
│ ├── __init__.py
│ ├── server.py # MCP-инструменты (stdio)
│ ├── database.py # read-only слой доступа к SQLite
│ ├── security.py # SQL validation + single-statement guard
│ ├── models.py # структуры результатов
│ └── config.py # SHOP_DB_PATH / MAX_RESULT_ROWS
└── tests/
├── test_database.py
├── test_security.py
├── test_tools.py
└── test_analytics.py
```
TDQS
Scored across 3 tools
Each tool serves a distinct, non-overlapping purpose: listing tables, describing a single table's schema, and executing read-only SQL queries. No ambiguity in selecting between them.
All tool names follow a consistent verb_noun snake_case pattern: list_tables, describe_table, read_query. This is uniform and predictable.
Three tools are exactly right for this focused read-only database exploration server. Each tool earns its place with no redundancy, and the count falls within the typical well-scoped range.
The tool surface covers the full workflow for safe database discovery: discover schema (list_tables), inspect structure (describe_table), and query data (read_query). No obvious gaps for the stated purpose, and write operations are intentionally excluded.