Skip to main content
Glama
stalexsm

shop-mcp

by stalexsm
README.md
# 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

A4.7/5.0

Scored across 3 tools

Disambiguation5/5

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.

Naming Consistency5/5

All tool names follow a consistent verb_noun snake_case pattern: list_tables, describe_table, read_query. This is uniform and predictable.

Tool Count5/5

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.

Completeness5/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues