# API Reference — VMK Data MCP Server

Детальное описание JSON-интерфейса каждого MCP-инструмента.

---

## Соглашения

- **Все текстовые запросы (`query`)** должны быть на **украинском языке**.
  AI-агент выполняет перевод перед вызовом инструмента.
- Все ответы сервера — **UTF-8 JSON**.
- Ошибки возвращаются как объект `{"error": "<сообщение>"}`.

---

## Общие типы

### `PaginationParams`

```json
{
  "limit": 20,
  "offset": 0
}
```

| Поле | Тип | По умолч. | Ограничения |
|------|-----|-----------|-------------|
| `limit` | `integer` | `20` | `1 ≤ limit ≤ 100` |
| `offset` | `integer` | `0` | `offset ≥ 0` |

### `MetadataFilters`

```json
{
  "deal_type": "sale",
  "city": "Київ",
  "district": "Печерський",
  "rooms_count": 2,
  "min_price": 50000,
  "max_price": 150000,
  "currency": "USD",
  "min_total_area": 50,
  "max_total_area": 100,
  "building_type": "monolith",
  "floor": 5,
  "listing_status": "active",
  "metro_station": "Арсенальна"
}
```

Все поля — **опциональные**. Указанные фильтры объединяются через `AND`.

| Поле | Тип | Допустимые значения |
|------|-----|---------------------|
| `deal_type` | `string` | `"sale"`, `"rent_long"`, `"rent_short"` |
| `city` | `string` | Любой (украинский) |
| `district` | `string` | Любой (украинский) |
| `rooms_count` | `integer` | `≥ 0` |
| `min_price` | `number` | `≥ 0` |
| `max_price` | `number` | `≥ 0` |
| `currency` | `string` | `"USD"`, `"EUR"`, `"UAH"` |
| `min_total_area` | `number` | `≥ 0` (м²) |
| `max_total_area` | `number` | `≥ 0` (м²) |
| `building_type` | `string` | `"brick"`, `"panel"`, `"monolith"`, `"gas_block"`, `"wood"` |
| `floor` | `integer` | `≥ 0` |
| `listing_status` | `string` | `"active"`, `"sold"`, `"rented"`, `"removed"`, `"archived"` |
| `metro_station` | `string` | Любой (украинский) |

### `ListingResult`

```json
{
  "id": 12345,
  "title": "2-кімнатна квартира в центрі Києва",
  "description": "...",
  "generated_description": "...",
  "price": 125000,
  "currency": "USD",
  "deal_type": "sale",
  "city": "Київ",
  "district": "Печерський",
  "rooms_count": 2,
  "total_area": 78.5,
  "living_area": 45.0,
  "kitchen_area": 12.0,
  "floor": 5,
  "floors_count": 12,
  "building_type": "monolith",
  "building_year": 2015,
  "renovation_status": "euro_repair",
  "balcony_count": 1,
  "bathroom_type": "combined",
  "parking_type": "underground",
  "heating_type": "central",
  "layout_type": "standard",
  "window_view": "courtyard",
  "metro_station": "Арсенальна",
  "metro_distance_type": "walking",
  "metro_distance_meters": 450,
  "url_source": "https://...",
  "publish_date": "2024-03-15",
  "images_count": 18,
  "contact_phone": "+380...",
  "listing_status": "active",
  "archived_at": null,
  "created_at": "2024-03-15",
  "updated_at": "2024-03-20",
  "similarity_score": 0.842,
  "rank_score": 0.123
}
```

| Поле | Тип | Примечание |
|------|-----|------------|
| `similarity_score` | `number \| null` | Только для `search_similar_listings` (cosine similarity ≈ 1 − distance/2) |
| `rank_score` | `number \| null` | Только для `search_by_metadata` (ts_rank_cd) |

### `SearchResult`

```json
{
  "total": 137,
  "limit": 20,
  "offset": 0,
  "listings": [ /* массив ListingResult */ ]
}
```

| Поле | Тип | Описание |
|------|-----|----------|
| `total` | `integer` | Общее количество записей, соответствующих фильтру (без `LIMIT`) |
| `limit` | `integer` | Фактический `limit` |
| `offset` | `integer` | Фактический `offset` |
| `listings` | `ListingResult[]` | Список объявлений |

---

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

### `search_similar_listings`

Векторный поиск по смыслу запроса. Использует `pgvector` + HNSW индекс.

**MCP-запрос (пример):**

```json
{
  "name": "search_similar_listings",
  "arguments": {
    "query": "сучасна 2-кімнатна квартира біля метро з ремонтом",
    "filters": {
      "city": "Київ",
      "deal_type": "sale",
      "min_price": 100000,
      "max_price": 200000,
      "currency": "USD"
    },
    "pagination": {
      "limit": 10,
      "offset": 0
    },
    "min_similarity": 0.75
  }
}
```

**Логика:**
1. Запрос отправляется в Ollama → получаем `embedding vector(768)`.
2. SQL:
   ```sql
   SELECT <USER_COLUMNS>,
          1 - (embedding <=> $1::vector) / 2.0 AS similarity_score
   FROM property_listings
   WHERE city = $2 AND deal_type = $3 AND price BETWEEN $4 AND $5
     AND embedding <=> $1::vector <= $6
   ORDER BY embedding <=> $1::vector
   LIMIT $7 OFFSET $8;
   ```
3. Дополнительный `COUNT(*)`-запрос для поля `total`.

**Ошибки:**
- `Сервис эмбеддингов недоступен` — Ollama недоступна
- `Ошибка при поиске` — проблема с БД или валидацией
- `Неожиданная ошибка` — внутренняя ошибка сервера

---

### `search_by_metadata`

Полнотекстовый поиск через `search_vector` (украинский FTS).

**MCP-запрос (пример):**

```json
{
  "name": "search_by_metadata",
  "arguments": {
    "query": "квартира Печерський район метро",
    "filters": {
      "listing_status": "active",
      "rooms_count": 3
    },
    "pagination": {
      "limit": 20,
      "offset": 0
    }
  }
}
```

**Логика:**
1. Запрос обрабатывается `plainto_tsquery('ukrainian', $1)`.
2. SQL:
   ```sql
   SELECT <USER_COLUMNS>,
          ts_rank_cd(search_vector, query) AS rank_score
   FROM property_listings,
        plainto_tsquery('ukrainian', $1) query
   WHERE search_vector @@ query
     AND listing_status = $2 AND rooms_count = $3
   ORDER BY rank_score DESC
   LIMIT $4 OFFSET $5;
   ```
3. Дополнительный `COUNT(*)`.

**Ошибки:** те же, что и для `search_similar_listings`, но без ошибок Ollama.

---

### `get_listing_by_id`

Получение одного объявления по `id`.

**MCP-запрос (пример):**

```json
{
  "name": "get_listing_by_id",
  "arguments": {
    "listing_id": 12345
  }
}
```

**Логика:**
```sql
SELECT <USER_COLUMNS>
FROM property_listings
WHERE id = $1;
```

**Ошибки:**
- `{"error": "Объявление не найдено"}` — если `id` отсутствует

---

### `describe_schema`

Описание схемы таблицы для подсказок AI-агенту.

**MCP-запрос (пример):**

```json
{
  "name": "describe_schema",
  "arguments": {}
}
```

**Возвращает** JSON-объект со списком колонок, их типами, индексами и
ограничениями, которые агент может использовать для формирования запросов.

---

## Коды ошибок HTTP транспорта

При работе через Streamable HTTP:

| Статус | Причина |
|--------|---------|
| `200` | Успешный SSE-стрим |
| `202` | Успешный POST (accept message) |
| `400` | Невалидный JSON или параметры |
| `404` | Endpoint не найден |
| `405` | Неподдерживаемый HTTP-метод |
| `500` | Внутренняя ошибка сервера |

---

## Примеры сценариев

### Сценарий 1: "Найди 3-комнатную квартиру в Киеве для покупки"

1. Агент переводит на украинский: `"3-кімнатна квартира Київ продаж"`.
2. Агент выбирает **векторный поиск** для семантической близости.
3. MCP-вызов:
   ```json
   {
     "name": "search_similar_listings",
     "arguments": {
       "query": "3-кімнатна квартира Київ продаж",
       "filters": { "city": "Київ", "deal_type": "sale", "rooms_count": 3 },
       "pagination": { "limit": 10, "offset": 0 },
       "min_similarity": 0.7
     }
   }
   ```

### Сценарий 2: "Есть ли квартиры у метро Арсенальная?"

1. Перевод: `"квартира біля метро Арсенальна"`.
2. **FTS-поиск** для точного ключевого слова "Арсенальна":
   ```json
   {
     "name": "search_by_metadata",
     "arguments": {
       "query": "квартира біля метро Арсенальна",
       "filters": { "metro_station": "Арсенальна", "listing_status": "active" },
       "pagination": { "limit": 20, "offset": 0 }
     }
   }
   ```

### Сценарий 3: "Покажи объявление № 12345"

```json
{
  "name": "get_listing_by_id",
  "arguments": { "listing_id": 12345 }
}
```
