| docs | 23 hours ago | ||
| src/ vmk_data_mcp | 23 hours ago | ||
| tests | 23 hours ago | ||
| .env.example | 23 hours ago | ||
| .gitignore | 23 hours ago | ||
| Dockerfile | 23 hours ago | ||
| README.md | 23 hours ago | ||
| docker-compose.yml | 23 hours ago | ||
| plan.md | 23 hours ago | ||
| pyproject.toml | 23 hours ago | ||
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 │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
/api/embed).vector.embedding <=> $1::vector <= $2.MCP-сервер рассчитывает, что в PostgreSQL уже существует подготовленная база vmk_data (или любая другая, указанная в DATABASE_URL).
Подключитесь к базе от имени суперпользователя и выполните:
-- 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
Сервер ожидает, что эмбеддинги уже сгенерированы и сохранены в БД.
Сервер регистрирует 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.
Шаг 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 строчки (см. раздел Быстрый старт → Вариант А).
pgvectornomic-embed-text (768d):
ollama pull nomic-embed-text
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.yml уже в репозитории. Он использует host.docker.internal для доступа к PostgreSQL и Ollama на хост-машине.
Linux:
host.docker.internalработает благодаряextra_hostsвdocker-compose.yml. На Windows/macOS это работает из коробки.
SET default_transaction_read_only = on._is_safe_query:
select, with, values, explain; внутри строки (блокировка multi-statement атак)USER_COLUMNS участвуют в SELECT; попытка запросить другую колонку вызывает ошибку.Все пользовательские данные передаются через параметризованные запросы ($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
Сервис эмбеддингов недоступен: ConnectError(...)
OLLAMA_BASE_URL0.0.0.0 (не только 127.0.0.1):
# На Linux: проверьте переменную окружения echo $OLLAMA_HOST # должно быть 0.0.0.0 или пусто
min_similarity — слишком высокий (0.95+) может исключить все записи.embedding (не NULL):
SELECT COUNT(*) FROM property_listings WHERE embedding IS NOT NULL;
vectorCREATE EXTENSION vector;
Сервер ожидает OLLAMA_EMBED_DIMENSIONS=768 (модель nomic-embed-text). Если используется другая модель — обновите переменную окружения.
search_vector не найденаЕсли в БД нет колонки search_vector — создайте её как generated column (см. раздел Требования к базе данных → Генерация search_vector).
MIT