Skip to main content
Glama
imangali01

mcp-postgres

by imangali01
README.md
# mcp-postgres

> **Хотите просто подключить сервер в Claude?** → [CLAUDE_SETUP.md](CLAUDE_SETUP.md)
> — пошаговая инструкция (Claude Code и Claude Desktop, со скриншотами). Дальше в
> этом файле — про то, как устроен сам проект.

Read-only MCP-сервер для PostgreSQL на Python.

Даёт LLM-клиенту (Claude Code, Claude Desktop, любой MCP-совместимый клиент) доступ
к базе **только на чтение**: интроспекция схемы и `SELECT`-запросы. Любые изменяющие
операции — `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, `ALTER`, `TRUNCATE`, `GRANT`,
`COPY`, `CALL` и т.д. — отклоняются.

Сервер доменно-нейтральный: он ничего не знает о бизнес-логике конкретной базы,
только механика доступа, защита и аудит.

---

## Какой транспорт когда запускать

| | **stdio** | **http** |
|---|---|---|
| Кто запускает процесс | MCP-клиент сам стартует сервер как дочерний процесс | вы: `docker compose up -d`, сервер работает постоянно |
| Где лежат креды к БД | в `mcp.json` у каждого пользователя | в `.env` на стороне сервера |
| Защита доступа | не нужна (процесс локальный) | bearer-токен `MCP_HTTP_AUTH_TOKEN` |
| Когда выбирать | персональный доступ аналитика к своей БД | общий сервис на команду / общая сервисная роль |

---

## Три уровня защиты от записи

1. **Валидатор SQL** ([sql_guard.py](mcp_postgres/sql_guard.py)) — разбирает запрос в AST
   (`sqlglot`, диалект postgres) и пропускает только `SELECT` / `WITH ... SELECT` / `VALUES`.
2. **Сессия Postgres** ([db.py](mcp_postgres/db.py)) — соединение открывается с
   `default_transaction_read_only=on`. Даже если валидатор пропустит запись, её отклонит сам Postgres.
3. **Права роли БД** — главный рубеж: отдельная роль с `GRANT SELECT`. Сервер не имеет
   инструмента смены кредов — какие права у роли, такие и у клиента.

---

## Инструменты

| Инструмент | Назначение |
|---|---|
| `connection_info` | К чему подключены, какая роль, какие лимиты и гарантии read-only |
| `list_schemas` | Схемы, доступные роли, с числом объектов |
| `list_tables` | Таблицы и представления: тип, размер, оценка строк, комментарий |
| `describe_table` | Колонки, типы, NOT NULL, DEFAULT, комментарии, ограничения, индексы |
| `list_indexes` | Индексы с определением, размером и статистикой использования |
| `table_stats` | Размеры, live/dead tuples, последний vacuum/analyze |
| `explain` | План выполнения (`EXPLAIN VERBOSE`); `ANALYZE` — только если явно разрешён |
| `query` | Выполнение читающего запроса. Параметры: `sql`, `params`, `max_rows`, `format` |

---

## Запуск: stdio (локально)

Нужен Python 3.11+. Устанавливать пакет не надо — только зависимости:

```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
```

В `mcp.json` клиента (полный пример — [examples/mcp.json](examples/mcp.json)):

```json
{
  "mcpServers": {
    "postgres": {
      "command": "/путь/к/репозиторию/.venv/bin/python",
      "args": ["/путь/к/репозиторию/run_server.py"],
      "env": {
        "PG_HOST": "db.example.com",
        "PG_PORT": "5432",
        "PG_DATABASE": "analytics",
        "PG_USER": "mcp_readonly",
        "PG_PASSWORD": "СЮДА_ПАРОЛЬ",
        "PG_SSLMODE": "require"
      }
    }
  }
}
```

## Запуск: http (Docker)

```bash
cp .env.example .env          # заполнить креды и MCP_HTTP_AUTH_TOKEN
docker compose up -d --build
curl -s localhost:8000/health
```

В `mcp.json` клиента:

```json
{
  "mcpServers": {
    "postgres": {
      "type": "http",
      "url": "http://127.0.0.1:8000/mcp",
      "headers": { "Authorization": "Bearer СЮДА_ТОКЕН" }
    }
  }
}
```

Логи — `docker compose logs -f`, остановить — `docker compose down`.

> На Linux каталог `./logs` монтируется внутрь контейнера: он должен быть доступен
> на запись пользователю `uid 10001`, иначе сервер откажется стартовать
> (`chown 10001 logs` или `chmod 777 logs`).

## Локальный Postgres для разработки

Нужен только для быстрой проверки, что сервер вообще работает — не для прод-данных:

```bash
docker compose -f docker-compose.postgres.yml up -d
```

Поднимает голый `postgres:15` с суперпользователем `postgres/postgres`. Оба
compose-файла используют общую сеть `mcp-net`, поэтому в `.env` сервера достаточно
`PG_HOST=postgres`. Данные лежат в `./data` (в git не попадают), `down` их не трогает.

Read-only роль под реальный доступ создаётся отдельно, вручную — см. SQL в разделе
[«Три уровня защиты»](#три-уровня-защиты-от-записи) выше.

---

## Конфигурация

Всё задаётся переменными окружения (в `mcp.json` → `env` или в `.env` для Docker).
Полный список с комментариями — в [.env.example](.env.example).

**Подключение:** `PG_DSN` (или `DATABASE_URL`) либо по частям — `PG_HOST`, `PG_PORT`,
`PG_DATABASE`, `PG_USER`, `PG_PASSWORD`, `PG_SSLMODE`.

**Транспорт:** `MCP_TRANSPORT` = `stdio` | `http`, `MCP_HTTP_HOST`, `MCP_HTTP_PORT`,
`MCP_HTTP_PATH`, `MCP_HTTP_AUTH_TOKEN`.

**Лимиты на запрос:** `MCP_MAX_ROWS` (1000), `MCP_MAX_RESULT_BYTES` (1 000 000),
`MCP_MAX_SQL_LENGTH` (20 000), `PG_STATEMENT_TIMEOUT_MS` (30 000), `MCP_ALLOWED_SCHEMAS`,
`MCP_ALLOW_EXPLAIN_ANALYZE` (по умолчанию `false`).

**Логи и аудит:** `LOG_LEVEL`, `LOG_FORMAT` (`text` | `json`), `LOG_FILE`, `LOG_SQL`, `AUDIT_FILE`.

---

## Логи и аудит

Два независимых потока.

**Логи работы сервера** — старт, подключение к БД, ошибки, отказы авторизации. Всегда идут
**в stderr** (и опционально в `LOG_FILE`): stdout занят JSON-RPC.

**Журнал аудита** — по одному JSONL-событию на каждый вызов инструмента: кто, что и когда
делал (`tool`, `status`, `db_user`/`db_name`, `sql`, `row_count` и т.д.), но без данных
результата — только их объём. Пишется в `AUDIT_FILE` (по умолчанию `./logs/audit.jsonl`);
если файл недоступен на запись, сервер не стартует.

---

## Тесты

Автотестами покрыт SQL-валидатор — та часть, где ошибка стоит дороже всего:

```bash
python -m pytest -q     # именно `python -m`, из корня репозитория
```

Остальное проверяется вручную из MCP-клиента: `connection_info` → `list_tables`
→ `describe_table` → `query`.

---

## Ограничения

- Одна база на процесс. Смена подключения на лету не предусмотрена намеренно:
  креды = identity, и менять их должен конфиг, а не модель.
- `params` работает с плейсхолдерами `%s` (psycopg), не `$1`.
- Курсоры/стриминг больших выборок не поддерживаются: ответ всегда ограничен
  `MCP_MAX_ROWS` и `MCP_MAX_RESULT_BYTES`.

Maintenance

ActivitySlowing
ResponsivenessNo issues