Newer
Older
vmk-360_data_mcp / README.md

VMK Data MCP Server

MCP-сервер (Model Context Protocol) для интеллектуального поиска по базе данных недвижимости vmk_data. Предоставляет AI-агентам (Claude, GPT и др.) безопасный набор инструментов для семантического и полнотекстового поиска объявлений с фильтрацией, пагинацией и сортировкой по релевантности.


📋 Содержание


Возможности

Функция Технология Описание
Семантический поиск pgvector + HNSW + Ollama Поиск объявлений «по смыслу» через векторную близость (cosine distance)
Полнотекстовый поиск PostgreSQL FTS (украинский конфиг) Поиск по ключевым словам с ранжированием по релевантности (ts_rank_cd)
Фильтрация метаданных SQL WHERE с параметрами Цена, район, комнаты, метро, тип сделки, статус и др.
Пагинация LIMIT / OFFSET Настраиваемый размер страницы (1–100)
Read-only безопасность default_transaction_read_only = on + валидация SQL Гарантированная защита от записи/изменения данных
Потоковый HTTP MCP Streamable HTTP Поддержка SSE-стрима + POST на порту 8080

Архитектура

┌─────────────────┐      HTTP (SSE+POST)      ┌──────────────────────────────┐
│   AI Агент      │  ───────────────────────>  │      VMK Data MCP Server      │
│  (Claude/GPT)   │      port 8080 /mcp       │      (FastMCP + Starlette)   │
└─────────────────┘                           └──────────────────────────────┘
                                                           │
                              ┌────────────────────────────┼────────────────────────────┐
                              │                            │                            │
                              ▼                            ▼                            ▼
                    ┌─────────────────┐          ┌─────────────────┐          ┌─────────────────┐
                    │   PostgreSQL   │          │    Ollama API   │          │   Логирование   │
                    │   vmk_data     │          │  nomic-embed-   │          │    (structlog)  │
                    │  + pgvector    │          │    text 768d    │          │                 │
                    └─────────────────┘          └─────────────────┘          └─────────────────┘

Поток данных при семантическом поиске

  1. Пользовательский запрос (на любом языке) переводится AI-агентом на украинский.
  2. MCP-сервер отправляет украинский текст в Ollama (/api/embed).
  3. Полученный вектор (768 float) передаётся в PostgreSQL как vector.
  4. pgvector выполняет поиск по HNSW-индексу: embedding <=> $1::vector <= $2.
  5. Результаты сериализуются в JSON и возвращаются агенту.

Требования к базе данных

MCP-сервер рассчитывает, что в PostgreSQL уже существует подготовленная база vmk_data (или любая другая, указанная в DATABASE_URL).

Обязательные расширения PostgreSQL

Подключитесь к базе от имени суперпользователя и выполните:

-- pgvector — для векторного поиска
CREATE EXTENSION IF NOT EXISTS vector;

-- Для FTS (обычно встроено, но проверьте)
CREATE EXTENSION IF NOT EXISTS pg_trgm;  -- опционально, для ILIKE-оптимизации

Таблица property_listings

Сервер читает только из этой таблицы. Она должна содержать следующие колонки:

Колонка Тип Обязательна Описание
id bigint Первичный ключ
title text Заголовок объявления
description text Описание
generated_description text AI-сгенерированное описание
price numeric Цена
currency varchar(3) USD, EUR, UAH
deal_type varchar sale, rent_long, rent_short
city varchar Город (украинский)
district varchar Район (украинский)
rooms_count int Количество комнат
total_area float Общая площадь, м²
living_area float Жилая площадь, м²
kitchen_area float Площадь кухни, м²
floor int Этаж
floors_count int Этажность дома
building_type varchar brick, panel, monolith, gas_block, wood
building_year int Год постройки
renovation_status varchar Статус ремонта
balcony_count int Количество балконов
bathroom_type varchar Тип санузла
parking_type varchar Тип парковки
heating_type varchar Тип отопления
layout_type varchar Тип планировки
window_view varchar Вид из окон
metro_station varchar Станция метро
metro_distance_type varchar walking, transport
metro_distance_meters int Расстояние до метро, м
url_source text Ссылка на источник
publish_date date Дата публикации
images_count int Количество фото
contact_phone varchar Телефон контакта
listing_status varchar active, sold, rented, removed, archived
archived_at date Дата архивации
created_at / updated_at date или timestamp Служебные таймстампы
embedding vector(768) Вектор эмбеддинга (pgvector)
search_vector tsvector Полнотекстовый индекс (украинский конфиг)

ℹ️ Если в вашей базе нет каких-то колонок (например, generated_description или kitchen_area), сервер всё равно запустится, но эти поля будут возвращаться как null. Обязательны только id, title, description, embedding, search_vector.

Индексы (обязательны для производительности)

-- Векторный HNSW-индекс (cosine distance)
CREATE INDEX IF NOT EXISTS property_listings_embedding_idx
ON property_listings
USING hnsw (embedding vector_cosine_ops);

-- Полнотекстовый GIN-индекс
CREATE INDEX IF NOT EXISTS property_listings_search_vector_idx
ON property_listings
USING gin (search_vector);

Генерация search_vector (если у вас его ещё нет)

Если колонка search_vector отсутствует, создайте её как generated column:

ALTER TABLE property_listings
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
    setweight(to_tsvector('ukrainian', coalesce(title, '')), 'A') ||
    setweight(to_tsvector('ukrainian', coalesce(description, '')), 'B') ||
    setweight(to_tsvector('ukrainian', coalesce(city, '')), 'C') ||
    setweight(to_tsvector('ukrainian', coalesce(district, '')), 'C') ||
    setweight(to_tsvector('ukrainian', coalesce(metro_station, '')), 'B')
) STORED;

-- Пересоздать GIN-индекс после добавления колонки
CREATE INDEX property_listings_search_vector_idx
ON property_listings USING gin (search_vector);

⚠️ Если украинский конфиг ukrainian отсутствует в SELECT cfgname FROM pg_ts_config;, замените 'ukrainian' на 'simple'.

Генерация embedding (если колонка пустая)

Для заполнения embedding в существующих записях используйте внешний скрипт (не входит в MCP-сервер). Примерная логика:

# Псевдокод — выполняется отдельно от MCP-сервера
for batch in chunks(listings_without_embedding, 100):
    texts = [f"{l.title}. {l.description}" for l in batch]
    vectors = ollama.embed(model="nomic-embed-text", input=texts)
    for l, v in zip(batch, vectors):
        UPDATE property_listings SET embedding = v::vector WHERE id = l.id

Сервер ожидает, что эмбеддинги уже сгенерированы и сохранены в БД.


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

Сервер регистрирует 4 инструмента, доступных через MCP-протокол:

search_similar_listings

Векторный (семантический) поиск — находит объявления, близкие по смыслу к запросу.

Входные параметры:

  • query (string, обязательный) — текст на украинском для эмбеддинга
  • filters (object) — фильтры метаданных (см. Фильтры)
  • pagination (object)limit (1–100, по умолч. 20), offset (≥ 0)
  • min_similarity (float) — порог косинусной близости (0.0–1.0, по умолч. 0.7)

Выход: SearchResult — список объявлений с полем similarity_score.

⚠️ Параметр min_similarity преобразуется в максимальное косинусное расстояние: max_distance = 2.0 × (1.0 − min_similarity), потому что оператор <=> в pgvector возвращает расстояние в диапазоне [0, 2].


search_by_metadata

Полнотекстовый поиск (FTS) — ищет по ключевым словам в search_vector.

Входные параметры:

  • query (string, обязательный) — текстовый запрос на украинском
  • filters (object) — те же фильтры, что и для векторного поиска
  • pagination (object) — пагинация

Выход: SearchResult — список с полем rank_score (релевантность FTS).


get_listing_by_id

Получение объявления по ID — точечная выборка одной записи.

Входные параметры:

  • listing_id (integer, обязательный)id объявления

Выход: ListingResult или {"error": "..."} если не найдено.


describe_schema

Описание схемы БД — возвращает структуру таблицы property_listings (колонки, типы, индексы) для подсказок AI-агенту при формировании запросов.

Входных параметров нет.

Выход: JSON-описание схемы.


Быстрый старт

Вариант А: Локальный запуск (для разработки)

Шаг 1 — Клонирование и окружение

git clone <repo-url>
cd data_mcp
python -m venv .venv
source .venv/bin/activate      # Linux / macOS
# .venv\Scripts\activate       # Windows PowerShell
pip install -e "."

Шаг 2 — Настройка .env

Скопируйте пример и отредактируйте:

cp .env.example .env
nano .env   # или vim / VS Code

Минимально необходимые переменные:

# Куда подключаться к PostgreSQL
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/vmk_data

# Куда обращаться за эмбеддингами
OLLAMA_BASE_URL=http://192.168.1.75:11434
OLLAMA_EMBED_MODEL=nomic-embed-text
OLLAMA_EMBED_DIMENSIONS=768

# На каком порту слушать MCP
MCP_PORT=8080

🔍 Где взять значения:

  • DATABASE_URL — DSN вашей PostgreSQL с базой vmk_data.
  • OLLAMA_BASE_URL — URL хоста, где запущена Ollama. Если на той же машине — http://localhost:11434.
  • OLLAMA_EMBED_MODEL — должна совпадать с моделью, которой вы генерировали embedding в БД.

Шаг 3 — Проверка зависимостей

# Проверьте, что Ollama отвечает
curl http://localhost:11434/api/tags

# Проверьте, что PostgreSQL доступна
psql "${DATABASE_URL}" -c "SELECT COUNT(*) FROM property_listings;"

Шаг 4 — Запуск

python -m vmk_data_mcp.main

В терминале появится:

INFO:     Started server process [12345]
INFO:     Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)

MCP endpoint доступен на http://localhost:8080/mcp.

Вариант Б: Docker Compose

Шаг 1 — Настройка .env

cp .env.example .env
# Отредактируйте — для Docker обычно нужен host.docker.internal

Пример .env для Docker на Linux:

DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5433/vmk_data
OLLAMA_BASE_URL=http://host.docker.internal:11434
MCP_PORT=8080

host.docker.internal — это хост-машина. Убедитесь, что PostgreSQL и Ollama слушают не только 127.0.0.1, а также внешний интерфейс (или настроен проброс портов).

Шаг 2 — Сборка и запуск

docker-compose up --build -d

Проверка:

curl http://localhost:8080/mcp
docker logs -f vmk-data-mcp

Остановка:

docker-compose down

Конфигурация

Все параметры задаются через .env или переменные окружения.

Обязательные переменные

Переменная Пример значения Откуда взять
DATABASE_URL postgresql://user:pass@host:5432/vmk_data DSN вашей PostgreSQL
OLLAMA_BASE_URL http://192.168.1.75:11434 URL сервера Ollama
OLLAMA_EMBED_MODEL nomic-embed-text Модель, использованная для генерации embedding в БД
OLLAMA_EMBED_DIMENSIONS 768 Размерность вектора модели (768 для nomic-embed-text)

Опциональные переменные

Переменная По умолчанию Описание
DB_POOL_MIN_SIZE 2 Минимум соединений в пуле asyncpg
DB_POOL_MAX_SIZE 10 Максимум соединений в пуле
DB_QUERY_TIMEOUT 30 Таймаут SQL-запросов, секунды
OLLAMA_REQUEST_TIMEOUT 60.0 Таймаут HTTP-запроса к Ollama, секунды
MCP_SERVER_NAME vmk-data-mcp Имя сервера в MCP-протоколе
MCP_PORT 8080 Порт HTTP-транспорта

💡 Совет: если Ollama и PostgreSQL работают локально, минимальный .env — это всего 4 строчки (см. раздел Быстрый старт → Вариант А).


Развёртывание

Требования к инфраструктуре

  • PostgreSQL 15+ с расширением pgvector
  • Ollama с моделью nomic-embed-text (768d):
    ollama pull nomic-embed-text
  • Python 3.11+ (если запускаете без Docker)

Docker (один контейнер)

docker build -t vmk-data-mcp .
docker run -d \
  -p 8080:8080 \
  -e DATABASE_URL=postgresql://... \
  -e OLLAMA_BASE_URL=http://... \
  --name vmk-data-mcp \
  vmk-data-mcp

Docker Compose (рекомендуется)

Файл docker-compose.yml уже в репозитории. Он использует host.docker.internal для доступа к PostgreSQL и Ollama на хост-машине.

Linux: host.docker.internal работает благодаря extra_hosts в docker-compose.yml. На Windows/macOS это работает из коробки.


Безопасность

Read-only гарантия

  1. На уровне соединения: каждое новое соединение получает SET default_transaction_read_only = on.
  2. На уровне строки: входящий SQL проверяется через _is_safe_query:
    • только префиксы select, with, values, explain
    • запрещён символ ; внутри строки (блокировка multi-statement атак)
  3. На уровне колонок: только USER_COLUMNS участвуют в SELECT; попытка запросить другую колонку вызывает ошибку.

SQL-инъекции

Все пользовательские данные передаются через параметризованные запросы ($1, $2 …). Ни один пользовательский параметр не интерполируется в строку SQL.


Разработка

Тесты

pytest -q

8 тестов покрывают:

  • границы пагинации (limit / offset)
  • валидацию фильтров
  • сериализацию моделей

Линтинг

ruff check src tests
ruff format src tests

Структура проекта

data_mcp/
├── src/vmk_data_mcp/
│   ├── __init__.py
│   ├── main.py          # FastMCP сервер + HTTP transport
│   ├── tools.py         # Реализация 4 инструментов
│   ├── models.py        # Pydantic-модели входных/выходных данных
│   ├── db.py            # asyncpg пул, read-only защита, USER_COLUMNS
│   ├── embedder.py      # Ollama HTTP клиент для эмбеддингов
│   └── config.py        # Настройки из .env (pydantic-settings)
├── tests/
│   └── test_models.py   # Pytest
├── docs/
│   ├── API.md           # JSON-схемы и примеры запросов
│   └── ARCHITECTURE.md  # Архитектура и data flow
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
├── .env.example
└── README.md

Решение проблем

Ollama недоступна

Сервис эмбеддингов недоступен: ConnectError(...)
  • Проверьте OLLAMA_BASE_URL
  • Убедитесь, что Ollama слушает на 0.0.0.0 (не только 127.0.0.1):
    # На Linux: проверьте переменную окружения
    echo $OLLAMA_HOST  # должно быть 0.0.0.0 или пусто
  • Проверьте firewall / Docker network

Нет результатов при семантическом поиске

  • Убедитесь, что запрос переведён на украинский перед вызовом инструмента.
  • Проверьте порог min_similarity — слишком высокий (0.95+) может исключить все записи.
  • Убедитесь, что в БД заполнена колонка embedding (не NULL):
    SELECT COUNT(*) FROM property_listings WHERE embedding IS NOT NULL;

PostgreSQL: отсутствует vector

CREATE EXTENSION vector;

Неправильная размерность эмбеддинга

Сервер ожидает OLLAMA_EMBED_DIMENSIONS=768 (модель nomic-embed-text). Если используется другая модель — обновите переменную окружения.

search_vector не найдена

Если в БД нет колонки search_vector — создайте её как generated column (см. раздел Требования к базе данных → Генерация search_vector).


Лицензия

MIT