data_collector сохраняет фото объявлений на диск в IMAGE_STORAGE_PATH и записывает метаданные в таблицу property_images:
property_id → внешний ключ на property_listings.idlocal_path — путь относительно корня хранилища, например 1/<sha256>.jpgurl — исходный URL (может быть temp-путём, не подходит для клиента)width, height, file_size, ai_description, order_indexMCP-сервер должен:
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).@mcp.custom_route("/images/{image_path:path}") на том же HTTP-сервере, что и MCP. Никакого дополнительного порта и второго приложения.IMAGE_STORAGE_PATH (может отличаться от data_collector, если директория примонтирована в другую точку).IMAGE_STORAGE_PATH (защита от ../../../etc/passwd).SELECT ... WHERE property_id = ANY($1) вместо N+1.get_listing_by_id — все доступные фото.image_base_url из конфига. Если не задан — генерировать относительные ссылки /images/<local_path>.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="Максимум фото на одно объявление в результатах поиска"
)
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="Фотографии объявления")
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] — одиночный запрос.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.
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..env.example: добавить IMAGE_STORAGE_PATH и IMAGE_BASE_URL.README.md: описать новые эндпоинт и поле images в ответах.describe_schema / SERVER_INSTRUCTIONS: упомянуть, что результаты теперь содержат ссылки на фото.tests/test_models.py)ListingImage.ListingResult с пустым списком images корректно сериализуется.data_collector по пути /var/lib/vmk/images, а MCP-сервер запускается на хосте. Для работы нужно либо примонтировать этот том к хосту, либо настроить IMAGE_STORAGE_PATH в MCP на точку, где файлы доступны. Код останется корректным при правильном конфиге.local_path в БД относительный, поэтому перемещение хранилища не требует миграции — достаточно изменить IMAGE_STORAGE_PATH.images.py с запросами к БД и построением URL.custom_route для раздачи файлов.get_listing_by_id и поисковых инструментов.describe_schema, README.md, .env.example.ruff./images/<local_path> и вызов get_listing_by_id с полем images.