# Архитектура VMK Data MCP Server

---

## Обзор системы

Сервер реализован как **MCP (Model Context Protocol) Server** на базе
`FastMCP` (Python SDK v1.x) с транспортом **Streamable HTTP**.
Он выступает безопасным посредником между AI-агентом и PostgreSQL-базой
недвижимости, предоставляя агенту инструменты поиска без прямого доступа к БД.

---

## Компоненты

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                            AI Агент (Claude / GPT)                          │
│                              MCP Client                                     │
└───────────────────────────────┬─────────────────────────────────────────────┘
                                │ HTTP SSE + POST
                                ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         VMK Data MCP Server                                 │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
│  │   main.py   │  │   tools.py  │  │   models.py │  │      embedder.py    │ │
│  │ FastMCP app │  │ 4 tools     │  │  Pydantic   │  │  httpx → Ollama     │ │
│  │ HTTP transport│  │ SQL builders│  │ validation  │  │  /api/embed         │ │
│  └──────┬──────┘  └──────┬──────┘  └─────────────┘  └─────────────────────┘ │
│         │                │                                                    │
│         └────────────────┼────────────────────────────────────────────────────┘
│                          │
│  ┌───────────────────────┴───────────────────────┐
│  │                  db.py                        │
│  │  asyncpg pool + read-only safety            │
│  │  USER_COLUMNS whitelist                       │
│  └───────────────────────┬───────────────────────┘
│                          │ asyncpg
└──────────────────────────┼───────────────────────────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                        PostgreSQL (vmk_data)                                │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────────────────┐ │
│  │  property_      │  │   embedding     │  │   search_vector             │ │
│  │  listings       │  │   vector(768)   │  │   tsvector (ukrainian)      │ │
│  │                 │  │   HNSW index    │  │   GIN index                 │ │
│  └─────────────────┘  └─────────────────┘  └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```

### 1. `main.py` — Entrypoint и FastMCP

- Создаёт экземпляр `FastMCP(name=settings.mcp_server_name)`.
- Регистрирует 4 инструмента через декоратор `@mcp.tool()`.
- Определяет `lifespan` — асинхронный контекстный менеджер для инициализации
  пула БД и закрытия ресурсов.
- Запускает `mcp.run(transport="streamable-http", port=settings.mcp_port)`.

**Streamable HTTP transport:**
- `GET /mcp` — SSE-стрим для сервер→клиент сообщений.
- `POST /mcp` — приём сообщений клиент→сервер.

### 2. `tools.py` — SQL-конструкторы и бизнес-логика

Каждый инструмент — async-функция, которая:
1. Принимает Pydantic-модель (`SearchSimilarInput`, `SearchMetadataInput` …).
2. Строит SQL через `_build_where_clause()` (параметризованный).
3. Для векторного поиска — вызывает `embedder.get_embedding(query)`.
4. Выполняет 2 запроса: `COUNT(*)` (для `total`) и `SELECT … LIMIT/OFFSET`.
5. Сериализует результат в `SearchResult` → JSON.

**Ключевая функция `_build_where_clause(filters)`: **
- Проверяет, что значение фильтра не `None`.
- Использует **strictly allowed columns** — только известные имена колонок.
- Генерирует SQL-условия с нумерованными параметрами `$N`.

### 3. `models.py` — Pydantic v2

- **Входные модели**: валидация типов, границ, enum-значений.
- **Выходные модели**: `ListingResult`, `SearchResult` — сериализация `asyncpg.Record`.
- Все модели наследуют `BaseModel` с чёткими `Field(description=…)`.

### 4. `db.py` — Безопасный доступ к БД

**Пул asyncpg:**
- `create_pool(..., init=_init_conn)` — каждое новое соединение получает
  `SET default_transaction_read_only = on`.
- Мин/макс размер пула и таймаут запросов из `.env`.

**Read-only защита `_is_safe_query(sql)`:**
- Разрешённые префиксы: `select`, `with`, `values`, `explain`.
- **Запрет `;` внутри строки** — блокировка multi-statement инъекций (`DROP TABLE;`).

**`USER_COLUMNS`:**
- Белый список колонок `property_listings`.
- `build_select_columns()` — динамически строит `SELECT col1, col2 …`.

### 5. `embedder.py` — Ollama клиент

- Singleton `httpx.AsyncClient` с `base_url` и таймаутом.
- `get_embedding(text)` → `list[float]` длины `OLLAMA_EMBED_DIMENSIONS`.
- Валидация размерности ответа.

### 6. `config.py` — Настройки

`pydantic-settings` загружает `.env` с `extra="ignore"`.
Все секреты и URL вынесены в переменные окружения.

---

## Потоки данных

### Векторный поиск (`search_similar_listings`)

```
┌─────────┐    украинский текст    ┌───────────┐
│  Agent  │ ───────────────────>  │ embedder  │
└─────────┘                       └─────┬─────┘
                                      │ HTTP POST /api/embed
                                      ▼
                               ┌──────────────┐
                               │   Ollama     │
                               │ 768d vector  │
                               └──────┬───────┘
                                      │
                                      ▼
┌─────────┐    SQL + $1..$N     ┌──────────┐    vector(768), max_distance
│  Agent  │ <─────────────────── │   db.py  │ <──────────────────────────────┐
└─────────┘    JSON result       └────┬─────┘                                  │
                                      │ asyncpg                               │
                                      ▼                                       │
                               ┌──────────────┐                               │
                               │  PostgreSQL  │                               │
                               │  HNSW scan   │                               │
                               │  embedding   │                               │
                               │  <=> vector  │                               │
                               └──────────────┘                               │
                                                                              │
                                        COSINE DISTANCE  [0, 2]               │
                                        max_distance = 2*(1 - similarity)   │
```

### Полнотекстовый поиск (`search_by_metadata`)

```
┌─────────┐    украинский текст    ┌──────────┐
│  Agent  │ ───────────────────>  │  tools   │
└─────────┘                       └────┬─────┘
                                     │
                                     ▼
                              ┌──────────────┐
                              │  PostgreSQL  │
                              │  plainto_ts- │
                              │  query('uk..')│
                              │  @@ search_  │
                              │  vector      │
                              └──────────────┘
```

---

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

| Индекс | Тип | Колонка | Оператор / Конфиг |
|--------|-----|---------|-------------------|
| `property_listings_embedding_idx` | **HNSW** | `embedding vector(768)` | `vector_cosine_ops` |
| `property_listings_search_vector_idx` | **GIN** | `search_vector` | `tsvector` (ukrainian) |

### Почему HNSW, а не IVFFlat?

- HNSW обеспечивает **лучшую точность** при высокой селективности фильтров.
- Не требует перестроения при добавлении данных (`ef_construction`, `m` настраиваются
  при создании).
- Рекомендуется для production-нагрузки с pgvector ≥ 0.5.0.

### Почему `vector_cosine_ops`?

- Модель `nomic-embed-text` генерирует **нормализованные** векторы.
- Cosine distance (`<=>`) эквивалентен Euclidean distance для нормализованных векторов,
  но явно выражает семантическую близость.

---

## Безопасность (Defense in Depth)

```
┌─────────────────────────────────────────────────────────────┐
│  Уровень 1: Сеть                                            │
│  — MCP Server доступен только внутри сети / через reverse   │
│    proxy с аутентификацией (не входит в скоуп проекта)      │
├─────────────────────────────────────────────────────────────┤
│  Уровень 2: PostgreSQL роли                               │
│  — Отдельный READ-ONLY пользователь БД (рекомендуется)      │
├─────────────────────────────────────────────────────────────┤
│  Уровень 3: default_transaction_read_only = on             │
│  — Каждая транзакция принудительно read-only                │
├─────────────────────────────────────────────────────────────┤
│  Уровень 4: Валидация SQL (_is_safe_query)                 │
│  — Только SELECT / WITH / VALUES / EXPLAIN                  │
│  — Блокировка `;` (multi-statement)                         │
├─────────────────────────────────────────────────────────────┤
│  Уровень 5: Параметризованные запросы                      │
│  — Никакого f-string / format в SQL                         │
├─────────────────────────────────────────────────────────────┤
│  Уровень 6: USER_COLUMNS whitelist                         │
│  — SELECT только разрешённые колонки                        │
└─────────────────────────────────────────────────────────────┘
```

---

## Масштабирование и производительность

| Параметр | Значение | Почему |
|----------|----------|--------|
| `DB_POOL_MAX_SIZE` | `10` | Оптимально для ~50–100 параллельных MCP-запросов |
| `DB_QUERY_TIMEOUT` | `30` | Защита от тяжёлых запросов без `LIMIT` |
| `LIMIT` | `1–100` | Жёсткий потолок на размер ответа |
| `Ollama timeout` | `60` | Эмбеддинг одного запроса ~300–800 мс |

### Потенциальные bottleneck'и

1. **Ollama** — если запросов много, стоит поднять Ollama на GPU или
   добавить кеширование эмбеддингов (Redis / in-memory LRU для популярных запросов).
2. **PostgreSQL CPU** — `COUNT(*)` с фильтрами на больших таблицах может
   требовать `pg_stat_statements` анализа и частичных индексов.
3. **HNSW `ef_search`** — при недостаточной точности можно увеличить
   `SET hnsw.ef_search = 100` (по умолчанию обычно 40).

---

## Логирование

Сервер использует стандартный `logging` из stdlib.

| Уровень | Что логируется |
|---------|----------------|
| `WARNING` | Ошибки Ollama, БД, валидации (кратко) |
| `ERROR` / `EXCEPTION` | Неожиданные ошибки в инструментах (с трейсбеком) |
| `INFO` | Старт/остановка сервера, lifespan события |

Пример:
```
WARNING  vmk_data_mcp.tools: Ollama error: ConnectError(...)
```
