# План: хостинг изображений объявлений в MCP-сервере

## Контекст

`data_collector` сохраняет фото объявлений на диск в `IMAGE_STORAGE_PATH` и записывает метаданные в таблицу `property_images`:
- `property_id` → внешний ключ на `property_listings.id`
- `local_path` — путь относительно корня хранилища, например `1/<sha256>.jpg`
- `url` — исходный URL (может быть temp-путём, не подходит для клиента)
- `width`, `height`, `file_size`, `ai_description`, `order_index`

MCP-сервер должен:
1. Раздавать файлы изображений по HTTP (хостить их сам).
2. Возвращать прямые ссылки на изображения внутри результатов `get_listing_by_id`, `search_similar_listings`, `search_by_metadata`.

## Текущее состояние

- `data_collector/.env`: `IMAGE_STORAGE_PATH=/var/lib/vmk/images` (prod-контейнер).
- `data_collector/.env.example`: `IMAGE_STORAGE_PATH=./data/images` (dev).
- В БД `property_images` содержит 5855 записей, все `local_path` относительные (`<property_id>/<hash>.jpg`).
- `FastMCP` поддерживает произвольные HTTP-маршруты через декоратор `custom_route`, поэтому можно добавить раздачу файлов на тот же порт, где работает MCP (по умолчанию 8080).

## Архитектурные решения

1. **Один порт, один процесс**: изображения раздаются через `@mcp.custom_route("/images/{image_path:path}")` на том же HTTP-сервере, что и MCP. Никакого дополнительного порта и второго приложения.
2. **Конфигурируемый путь к хранилищу**: MCP-сервер получает собственный `IMAGE_STORAGE_PATH` (может отличаться от `data_collector`, если директория примонтирована в другую точку).
3. **Безопасность файловой системы**: любой запрошенный путь разрешается в абсолютный и проверяется, что он находится внутри `IMAGE_STORAGE_PATH` (защита от `../../../etc/passwd`).
4. **Пакетная загрузка метаданных изображений**: для поисковых запросов изображения запрашиваются одним `SELECT ... WHERE property_id = ANY($1)` вместо N+1.
5. **Ограничение количества фото в списке**: в результатах поиска отдавать первые 5 изображений, чтобы не раздувать JSON. В `get_listing_by_id` — все доступные фото.
6. **URL-ы**: `image_base_url` из конфига. Если не задан — генерировать относительные ссылки `/images/<local_path>`.

## Изменения в коде

### 1. Конфигурация (`src/vmk_data_mcp/config.py`)

Добавить поля:
```python
image_storage_path: str = Field(
    default="/var/lib/vmk/images",
    description="Абсолютный путь к корню хранилища изображений на файловой системе MCP-сервера",
)
image_base_url: str = Field(
    default="",
    description="Базовый URL для ссылок на изображения. Пустое значение → /images/<local_path>",
)
max_images_in_search: int = Field(
    default=5, ge=0, description="Максимум фото на одно объявление в результатах поиска"
)
```

### 2. Модели (`src/vmk_data_mcp/models.py`)

Добавить:
```python
class ListingImage(BaseModel):
    url: str = Field(description="Прямая ссылка на изображение")
    width: int | None = None
    height: int | None = None
    file_size: int | None = None
    ai_description: str | None = None
    order_index: int | None = None
```

В `ListingResult` добавить поле:
```python
images: list[ListingImage] = Field(default_factory=list, description="Фотографии объявления")
```

### 3. Модуль работы с изображениями (`src/vmk_data_mcp/images.py`)

Ответственности:
- `resolve_image_path(local_path: str) -> Path | None` — разрешение относительного/абсолютного пути с проверкой внутри `image_storage_path`.
- `build_image_url(local_path: str) -> str` — построение публичного URL.
- `fetch_images_for_listings(pool, listing_ids: list[int]) -> dict[int, list[ListingImage]]` — batch-запрос в `property_images` и группировка.
- `fetch_images_for_listing(pool, listing_id: int) -> list[ListingImage]` — одиночный запрос.

### 4. Раздача файлов (`src/vmk_data_mcp/main.py`)

После создания `mcp = FastMCP(...)` добавить:
```python
@mcp.custom_route("/images/{image_path:path}", methods=["GET"])
async def serve_image(request: Request) -> Response:
    ...
```

Использовать `starlette.responses.FileResponse` и `starlette.requests.Request`.

### 5. Обогащение результатов (`src/vmk_data_mcp/tools.py`)

- `get_listing_by_id`: после получения строки объявления вызвать `fetch_images_for_listing` и встроить в `ListingResult`.
- `search_similar_listings` / `search_by_metadata`: после формирования списка `listing_ids` вызвать `fetch_images_for_listings` с ограничением `max_images_in_search`, прикрепить к каждому `ListingResult`.

### 6. Документация и конфиг

- `.env.example`: добавить `IMAGE_STORAGE_PATH` и `IMAGE_BASE_URL`.
- `README.md`: описать новые эндпоинт и поле `images` в ответах.
- `describe_schema` / `SERVER_INSTRUCTIONS`: упомянуть, что результаты теперь содержат ссылки на фото.

### 7. Тесты (`tests/test_models.py`)

- Проверить сериализацию `ListingImage`.
- Проверить, что `ListingResult` с пустым списком `images` корректно сериализуется.

## Риски и нюансы

- В dev-окружении файлы могут лежать внутри Docker-контейнера `data_collector` по пути `/var/lib/vmk/images`, а MCP-сервер запускается на хосте. Для работы нужно либо примонтировать этот том к хосту, либо настроить `IMAGE_STORAGE_PATH` в MCP на точку, где файлы доступны. Код останется корректным при правильном конфиге.
- Путь `local_path` в БД относительный, поэтому перемещение хранилища не требует миграции — достаточно изменить `IMAGE_STORAGE_PATH`.

## Порядок реализации

1. Конфиг + модели.
2. Модуль `images.py` с запросами к БД и построением URL.
3. `custom_route` для раздачи файлов.
4. Обогащение `get_listing_by_id` и поисковых инструментов.
5. Обновление `describe_schema`, `README.md`, `.env.example`.
6. Обновление тестов и проверка `ruff`.
7. Ручной smoke-test: запрос изображения по `/images/<local_path>` и вызов `get_listing_by_id` с полем `images`.
