Newer
Older
vmk-360_data_mcp / plan.md

План: хостинг изображений объявлений в 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)

Добавить поля:

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)

Добавить:

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 добавить поле:

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(...) добавить:

@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.