# VMK Data MCP Server

MCP-сервер (Model Context Protocol) для интеллектуального поиска по базе данных
недвижимости **`vmk_data`**. Предоставляет AI-агентам (Claude, GPT и др.) безопасный
набор инструментов для семантического и полнотекстового поиска объявлений с
фильтрацией, пагинацией и сортировкой по релевантности.

---

## 📋 Содержание

- [Возможности](#возможности)
- [Архитектура](#архитектура)
- [Инструменты MCP](#инструменты-mcp)
- [Модель данных](#модель-данных)
- [Быстрый старт](#быстрый-старт)
- [Конфигурация](#конфигурация)
- [Развёртывание](#развёртывание)
- [Безопасность](#безопасность)
- [Разработка](#разработка)
- [Решение проблем](#решение-проблем)

---

## Возможности

| Функция | Технология | Описание |
|---------|-----------|----------|
| **Семантический поиск** | `pgvector` + HNSW + Ollama | Поиск объявлений «по смыслу» через векторную близость (cosine distance) |
| **Полнотекстовый поиск** | PostgreSQL FTS (украинский конфиг) | Поиск по ключевым словам с ранжированием по релевантности (`ts_rank_cd`) |
| **Фильтрация метаданных** | SQL `WHERE` с параметрами | Цена, район, комнаты, метро, тип сделки, статус и др. |
| **Пагинация** | `LIMIT` / `OFFSET` | Настраиваемый размер страницы (1–100) |
| **Read-only безопасность** | `default_transaction_read_only = on` + валидация SQL | Гарантированная защита от записи/изменения данных |
| **Потоковый HTTP** | MCP Streamable HTTP | Поддержка SSE-стрима + POST на порту 8080 |

---

## Архитектура

```
┌─────────────────┐      HTTP (SSE+POST)      ┌──────────────────────────────┐
│   AI Агент      │  ───────────────────────>  │      VMK Data MCP Server      │
│  (Claude/GPT)   │      port 8080 /mcp       │      (FastMCP + Starlette)   │
└─────────────────┘                           └──────────────────────────────┘
                                                           │
                              ┌────────────────────────────┼────────────────────────────┐
                              │                            │                            │
                              ▼                            ▼                            ▼
                    ┌─────────────────┐          ┌─────────────────┐          ┌─────────────────┐
                    │   PostgreSQL   │          │    Ollama API   │          │   Логирование   │
                    │   vmk_data     │          │  nomic-embed-   │          │    (structlog)  │
                    │  + pgvector    │          │    text 768d    │          │                 │
                    └─────────────────┘          └─────────────────┘          └─────────────────┘
```

### Поток данных при семантическом поиске

1. Пользовательский запрос (на любом языке) переводится AI-агентом на **украинский**.
2. MCP-сервер отправляет украинский текст в **Ollama** (`/api/embed`).
3. Полученный вектор (768 float) передаётся в PostgreSQL как `vector`.
4. pgvector выполняет поиск по HNSW-индексу: `embedding <=> $1::vector <= $2`.
5. Результаты сериализуются в JSON и возвращаются агенту.

---

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

Сервер регистрирует 4 инструмента, доступных через MCP-протокол:

### `search_similar_listings`
**Векторный (семантический) поиск** — находит объявления, близкие по смыслу к запросу.

**Входные параметры:**
- `query` *(string, обязательный)* — текст на **украинском** для эмбеддинга
- `filters` *(object)* — фильтры метаданных (см. [Фильтры](#фильтры))
- `pagination` *(object)* — `limit` (1–100, по умолч. 20), `offset` (≥ 0)
- `min_similarity` *(float)* — порог косинусной близости (0.0–1.0, по умолч. 0.7)

**Выход:** [`SearchResult`](#searchresult) — список объявлений с полем `similarity_score`.

> ⚠️ Параметр `min_similarity` преобразуется в максимальное косинусное расстояние:
> `max_distance = 2.0 × (1.0 − min_similarity)`, потому что оператор `<=>` в pgvector
> возвращает расстояние в диапазоне **[0, 2]**.

---

### `search_by_metadata`
**Полнотекстовый поиск (FTS)** — ищет по ключевым словам в `search_vector`.

**Входные параметры:**
- `query` *(string, обязательный)* — текстовый запрос на украинском
- `filters` *(object)* — те же фильтры, что и для векторного поиска
- `pagination` *(object)* — пагинация

**Выход:** [`SearchResult`](#searchresult) — список с полем `rank_score` (релевантность FTS).

---

### `get_listing_by_id`
**Получение объявления по ID** — точечная выборка одной записи.

**Входные параметры:**
- `listing_id` *(integer, обязательный)* — `id` объявления

**Выход:** [`ListingResult`](#listingresult) или `{"error": "..."}` если не найдено.

---

### `describe_schema`
**Описание схемы БД** — возвращает структуру таблицы `property_listings`
(колонки, типы, индексы) для подсказок AI-агенту при формировании запросов.

**Входных параметров нет.**

**Выход:** JSON-описание схемы.

---

## Модель данных

### Таблица `property_listings`

Ключевые колонки, доступные для чтения (белый список `USER_COLUMNS`):

| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | `bigint` | Первичный ключ |
| `title` | `text` | Заголовок объявления |
| `description` | `text` | Описание |
| `generated_description` | `text` | AI-сгенерированное описание |
| `price` | `numeric` | Цена |
| `currency` | `varchar(3)` | Валюта: `USD`, `EUR`, `UAH` |
| `deal_type` | `varchar` | Тип сделки: `sale`, `rent_long`, `rent_short` |
| `city` | `varchar` | Город (украинский) |
| `district` | `varchar` | Район (украинский) |
| `rooms_count` | `int` | Количество комнат |
| `total_area` | `float` | Общая площадь, м² |
| `living_area` | `float` | Жилая площадь, м² |
| `kitchen_area` | `float` | Площадь кухни, м² |
| `floor` | `int` | Этаж |
| `floors_count` | `int` | Этажность дома |
| `building_type` | `varchar` | `brick`, `panel`, `monolith`, `gas_block`, `wood` |
| `building_year` | `int` | Год постройки |
| `renovation_status` | `varchar` | Статус ремонта |
| `balcony_count` | `int` | Количество балконов |
| `bathroom_type` | `varchar` | Тип санузла |
| `parking_type` | `varchar` | Тип парковки |
| `heating_type` | `varchar` | Тип отопления |
| `layout_type` | `varchar` | Тип планировки |
| `window_view` | `varchar` | Вид из окон |
| `metro_station` | `varchar` | Станция метро |
| `metro_distance_type` | `varchar` | `walking`, `transport` |
| `metro_distance_meters` | `int` | Расстояние до метро, м |
| `url_source` | `text` | Ссылка на источник |
| `publish_date` | `date` | Дата публикации |
| `images_count` | `int` | Количество фото |
| `contact_phone` | `varchar` | Телефон контакта |
| `listing_status` | `varchar` | `active`, `sold`, `rented`, `removed`, `archived` |
| `archived_at` | `date` | Дата архивации |
| `created_at` / `updated_at` | `date` | Служебные таймстампы |
| `embedding` | `vector(768)` | Вектор эмбеддинга (pgvector) |
| `search_vector` | `tsvector` | Полнотекстовый индекс (украинский конфиг) |

### Индексы базы данных

- `property_listings_embedding_idx` — **HNSW** на `embedding` (`vector_cosine_ops`)
- `property_listings_search_vector_idx` — **GIN** на `search_vector`

---

## Быстрый старт

### 1. Клонирование и установка

```bash
git clone <repo-url>
cd data_mcp
python -m venv .venv
source .venv/bin/activate  # Linux/macOS
# .venv\Scripts\activate   # Windows
pip install -e "."
```

### 2. Настройка окружения

```bash
cp .env.example .env
# Отредактируйте .env — укажите DATABASE_URL и OLLAMA_BASE_URL
```

### 3. Запуск

```bash
python -m vmk_data_mcp.main
# или
uvicorn vmk_data_mcp.main:app --host 0.0.0.0 --port 8080
```

Сервер стартует на `http://localhost:8080/mcp`.

---

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

Все параметры задаются через `.env` или переменные окружения:

| Переменная | По умолчанию | Описание |
|------------|-------------|----------|
| `DATABASE_URL` | `postgresql://postgres:postgres@localhost:5432/vmk_data` | DSN для asyncpg |
| `DB_POOL_MIN_SIZE` | `2` | Минимум соединений в пуле |
| `DB_POOL_MAX_SIZE` | `10` | Максимум соединений в пуле |
| `DB_QUERY_TIMEOUT` | `30` | Таймаут SQL-запросов, сек |
| `OLLAMA_BASE_URL` | `http://192.168.1.75:11434` | URL Ollama API |
| `OLLAMA_EMBED_MODEL` | `nomic-embed-text` | Модель эмбеддинга |
| `OLLAMA_EMBED_DIMENSIONS` | `768` | Размерность вектора |
| `OLLAMA_REQUEST_TIMEOUT` | `60.0` | Таймаут запроса к Ollama, сек |
| `MCP_SERVER_NAME` | `vmk-data-mcp` | Имя сервера в MCP |
| `MCP_PORT` | `8080` | Порт HTTP-транспорта |

---

## Развёртывание

### Docker Compose (рекомендуется)

```bash
docker-compose up --build -d
```

`docker-compose.yml` использует `host.docker.internal` для доступа к
PostgreSQL и Ollama, запущенным на хост-машине.

> **Linux:** `host.docker.internal` требует `extra_hosts: ["host.docker.internal:host-gateway"]`
> (уже прописано в `docker-compose.yml`).

### Требования к инфраструктуре

- **PostgreSQL 15+** с расширениями:
  ```sql
  CREATE EXTENSION IF NOT EXISTS vector;
  CREATE EXTENSION IF NOT EXISTS pg_trgm;  -- опционально
  ```
- **Ollama** с моделью `nomic-embed-text` (768 dimensions):
  ```bash
  ollama pull nomic-embed-text
  ```
- Для `search_vector` требуется украинская конфигурация FTS (либо `simple`,
  если украинский конфиг отсутствует — проверьте `pg_ts_config`).

---

## Безопасность

### Read-only гарантия

1. **На уровне соединения:** каждое новое соединение получает
   `SET default_transaction_read_only = on`.
2. **На уровне строки:** входящий SQL проверяется через `_is_safe_query`:
   - только префиксы `select`, `with`, `values`, `explain`
   - запрещён символ `;` внутри строки (блокировка multi-statement атак)
3. **На уровне колонок:** только `USER_COLUMNS` участвуют в `SELECT`;
попытка запросить другую колонку вызывает ошибку.

### SQL-инъекции

Все пользовательские данные передаются через **параметризованные запросы** (`$1, $2 …`).
Ни один пользовательский параметр не интерполируется в строку SQL.

---

## Разработка

### Тесты

```bash
pytest -q
```

8 тестов покрывают:
- границы пагинации (`limit` / `offset`)
- валидацию фильтров
- сериализацию моделей

### Линтинг

```bash
ruff check src tests
ruff format src tests
```

### Структура проекта

```
data_mcp/
├── src/vmk_data_mcp/
│   ├── __init__.py
│   ├── main.py          # FastMCP сервер + HTTP transport
│   ├── tools.py         # Реализация 4 инструментов
│   ├── models.py        # Pydantic-модели входных/выходных данных
│   ├── db.py            # asyncpg пул, read-only защита, USER_COLUMNS
│   ├── embedder.py      # Ollama HTTP клиент для эмбеддингов
│   └── config.py        # Настройки из .env (pydantic-settings)
├── tests/
│   └── test_models.py   # Pytest
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
├── .env.example
└── README.md
```

---

## Решение проблем

### Ollama недоступна

```
Сервис эмбеддингов недоступен: ConnectError(...)
```
- Проверьте `OLLAMA_BASE_URL`
- Убедитесь, что Ollama слушает на `0.0.0.0` (не только `127.0.0.1`)
- Проверьте firewall / Docker network

### Нет результатов при семантическом поиске

- Убедитесь, что запрос переведён на **украинский** перед вызовом инструмента.
- Проверьте порог `min_similarity` — слишком высокий (0.95+) может
  исключить все записи.
- Убедитесь, что в БД заполнена колонка `embedding` (не NULL).

### PostgreSQL: отсутствует `vector`

```sql
CREATE EXTENSION vector;
```

### Неправильная размерность эмбеддинга

Сервер ожидает `OLLAMA_EMBED_DIMENSIONS=768` (модель `nomic-embed-text`).
Если используется другая модель — обновите переменную окружения.

---

## Лицензия

MIT
