Skip to main content
Glama

mcp_feast

MCP-сервер поверх хранилища признаков Feast для модели обнаружения мошенничества при оплате картой. Работает полностью локально: Parquet-офлайн-хранилище, SQLite-онлайн-хранилище, без облака и без брокера.


Архитектура системы

Четыре уровня

flowchart TB
    subgraph H["HOST — decides which tools to call"]
        direction LR
        H1["host.py<br/><i>local LLM, qwen2.5:7b</i>"]
        H2["Claude Code<br/><i>.mcp.json</i>"]
        H3["mcp_cli.py<br/><i>manual, for testing</i>"]
    end

    subgraph M["MCP SERVER — no Feast import, no credentials"]
        M1["12 read tools<br/>+ 2 gated write tools"]
    end

    subgraph A["FEATURE API — holds the Feast SDK"]
        A1["catalog"]
        A2["lineage"]
        A3["health"]
        A4["values"]
    end

    subgraph S["STORAGE"]
        direction LR
        S1[("registry.db<br/><i>metadata</i>")]
        S2[("online_store.db<br/><i>SQLite, serving</i>")]
        S3[("data/*.parquet<br/><i>offline</i>")]
    end

    H1 -->|"stdio"| M1
    H2 -->|"stdio"| M1
    H3 -->|"stdio"| M1
    M1 ==>|"HTTP / JSON"| A1
    A1 -->|"Feast SDK"| S1
    A2 --> S1
    A3 --> S3
    A4 --> S2

Эта толстая стрелка и есть вся архитектура. Всё, что специфично для Feast, находится ниже неё. MCP-серверу выше неё не нужен установленный Feast, драйверы хранилищ или учётные данные для хранилища данных — он является HTTP-клиентом и ничем более.

Это даёт три преимущества. Замена SQLite на Redis становится изменением в feature_store.yaml, которое MCP-уровень никогда не увидит. Ноутбуку, на котором работает MCP-сервер, нужен один доступный URL вместо сетевого маршрута к продакшен-Redis. И тот же API может обслуживать второго потребителя — сервер моделей, который здесь не создавался, но вызывал бы POST /features/online точно так же, как это делает MCP-уровень.

Related MCP server: Foundry Agents MCP Server

Что реально запускается

Процесс

Запускается с помощью

Владеет

Порт

Feature API

./run_api.sh

синглтоном FeatureStore

8000

MCP-сервер

хостом через stdio

клиентом httpx

—

Ollama

ollama serve

qwen2.5:7b

11434

Хост

python3 host.py

циклом диалога

—

Только API импортирует Feast. Проверьте это:

python3 -c "import mcp_server.server, sys; print('feast' in sys.modules)"   # False

Один запрос от начала до конца

Вопрос «почему карта C-4471 была помечена?» пересекает каждый уровень дважды:

sequenceDiagram
    autonumber
    participant L as Model
    participant M as MCP server
    participant A as Feature API
    participant F as Feast SDK
    participant D as SQLite

    L->>M: resolve_card("C-4471")
    M->>A: GET /cards/C-4471
    A-->>M: CU-8842
    M-->>L: C-4471 is owned by CU-8842

    Note over L: the model spans two entities,<br/>so both join keys are needed

    L->>M: explain_features_for_entity(card + customer)
    M->>A: POST /features/explain
    A->>F: get_online_features(fraud_model_v2)
    F->>D: read 7 values
    A->>F: provider.online_read(...)
    F->>D: read per-entity event_ts
    Note over A: joins values against TTL<br/>to classify each feature
    A-->>M: values + age + is_stale + reasons
    M-->>L: FRESH 6 / STALE 0 / MISSING 1

Этот второй вызов SDK — та часть, которую Feast не даёт бесплатно — см. ниже.

Как данные попадают в онлайн-хранилище

flowchart LR
    P[("data/*.parquet<br/>offline store")]
    O[("online_store.db<br/>online store")]
    W["live swipe"]
    R["serving<br/><i>milliseconds</i>"]
    T["training set"]

    P -->|"feast materialize — batch, scheduled"| O
    W -->|"feast push — real time, no broker"| O
    O -->|"get_online_features"| R
    P -.->|"get_historical_features — not exposed"| T

Пунктирный путь — это обучающая половина хранилища признаков. Он намеренно опущен: он выполняет запрос, который длится минуты и возвращает миллионы строк, что не подходит для чат-инструмента. Именно поэтому генератор не записывает метки мошенничества.

Почему API не является прокси-передачей

get_online_features() возвращает только значения и ничего больше. Голый null не может сказать, в какой из четырёх ситуаций вы находитесь — а Feast без жалоб выдаёт просроченное значение:

flowchart LR
    B["get_online_features<br/><b>txn_count_1h: null</b>"]
    B --> C1["<b>ENTITY_NOT_FOUND</b><br/>no row for this card"]
    B --> C2["<b>NULL_IN_SOURCE</b><br/>feature genuinely absent"]
    B --> C3["<b>STALE</b><br/>6h58m old, TTL is 2h"]
    B --> C4["<b>a real zero</b><br/>the card had no swipes"]

POST /features/explain разделяет их, восстанавливая event_timestamp для каждой сущности через online_read провайдера — тот же вызов, который get_online_features делает внутри, но который показывает временную метку — и сопоставляя её с TTL представления.

Три факта, которые сырой SDK не даст:

Endpoint

Выводит

/features/explain

свежесть по каждому признаку и причину отсутствия значения

/features/{view}/{feature}/lineage

источник → представление → потребляющие сервисы

/feature-views/{name}/consumers

радиус поражения перед изменением

Ловушка, для избежания которой это построено

Свежесть — по сущности, а не по представлению. Оба вопроса реальны и имеют разные ответы, и их путаница — самая опасная ошибка, доступная здесь:

flowchart TB
    V["<b>card_velocity</b><br/>materialized 52 seconds ago<br/>check_feature_freshness reports OK"]
    V -->|"source had a row from 58m ago"| E1["<b>C-4471</b><br/>age 58m<br/>FRESH"]
    V -->|"source's newest row is 6h58m old"| E2["<b>C-7788</b><br/>age 6h58m<br/>STALE"]

    style E1 stroke:#2a9d4a,stroke-width:2px
    style E2 stroke:#d1443c,stroke-width:3px

Материализация записывает то, что содержит источник. Для карты без недавних строк это старое значение — поэтому сущность может быть на часы устаревшей внутри представления, которое материализовалось секунды назад. Обновление представления не может это исправить; только push может.

Вопрос

Инструмент

Область

«Мёртв ли конвейер?»

check_feature_freshness

все сущности

«Актуальна ли эта карта?»

explain_features_for_entity

одна сущность

Небольшая модель стабильно путает эти понятия. Что исправило это — не системный промпт, а добавление предупреждения в вывод check_feature_freshness. Модель, которая пропускает описание инструмента, всё равно читает результат, на который она только что действовала.

Инструменты сопоставляются с эндпоинтами один к одному

flowchart LR
    T1["list_feature_views<br/>describe_feature_view<br/>list_feature_services<br/>search_features<br/>list_entities<br/>resolve_card"] --> E1["/entities · /data-sources<br/>/feature-views · /feature-services<br/>/features/search · /cards"]
    T2["get_feature_lineage<br/>get_feature_consumers"] --> E2["/features/../lineage<br/>/feature-views/../consumers"]
    T3["check_feature_freshness"] --> E3["/health/materialization"]
    T4["get_online_features<br/>explain_features_for_entity"] --> E4["/features/online<br/>/features/explain"]
    T5["push_swipe<br/>trigger_materialization"] -.->|"only when FEAST_MCP_READONLY=false"| E5["/features/push<br/>/feature-views/../materialize"]

api/routers/ и mcp_server/tools/ зеркально отражают друг друга файл за файлом — каталог, происхождение, здоровье, значения — так что навигация очевидна.

Две идеи, которые определили дизайн

Ошибки записаны как инструкции. 404 возвращает Available: [...], а строка с неверной сущностью называет ключи соединения, которые ей нужны. Наблюдалось многократно: модель 7B ошибается, читает ошибку и исправляется на следующем шаге, а не угадывает снова.

Руководство передаётся через вывод, а не только через описания. Описания инструментов пропускаются; результаты — нет. И предупреждение о области свежести, и «вызовите check_feature_freshness для подтверждения» в trigger_materialization находятся в возвращаемом тексте, и оба изменили поведение модели, когда только формулировка промпта не сработала.


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

Python 3.11. Feast объявляет >=3.10, но классифицирует только 3.10, и его транзитивный стек — обычный источник проблем на более новых интерпретаторах.

python3.11 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

./setup.sh        # preflight + data + apply + materialize
./run_api.sh      # API on :8000, docs at /docs

Оба скрипта учитывают переменную PYTHON, если зависимости находятся в другом месте:

PYTHON=/opt/miniconda3/envs/myenv/bin/python3 ./setup.sh

MCP-сервер запускается хостом через .mcp.json, который фиксирует абсолютный путь к интерпретатору по причине, указанной в разделе «Устранение неполадок» ниже. ./run_mcp.sh запускает его вручную для отладки.

Устранение неполадок: неверный интерпретатор

Два симптома, одна причина — другой Python, чем тот, в котором находятся зависимости:

ModuleNotFoundError: No module named 'feast'
ImportError: cannot import name 'MCPServer' from 'mcp.server'

Второй — более коварный: mcp 1.x импортируется нормально, но предоставляет mcp.server.fastmcp.FastMCP, а не mcp.server.MCPServer 2.x, который используется в этом проекте. Приглашение оболочки с активной средой conda — не доказательство; проверьте PATH:

which python3 && python3 -V
echo $PATH | tr ":" "\n" | head -3

Если системный Python или Python фреймворка находится перед вашей средой, каждый вызов python3 выходит из среды, независимо от того, что говорит приглашение. Диагностируйте правильно с помощью:

python3 preflight.py

Он импортирует точный символ, который нужен каждой части кода — не только модуль — так что зависимость с неверной мажорной версией обнаруживается по имени, и он предупреждает, когда uvicorn или feast в вашем PATH принадлежат другой среде.

Каждая точка входа принимает переопределение PYTHON, так что вам никогда не придётся бороться с PATH:

PYTHON=/opt/miniconda3/envs/myenv/bin/python3 ./setup.sh
PYTHON=/opt/miniconda3/envs/myenv/bin/python3 ./run_api.sh
PYTHON=/opt/miniconda3/envs/myenv/bin/python3 python3 mcp_cli.py tools

Два правила полностью избегают этого:

  • Запускайте API с помощью ./run_api.sh или python3 -m uvicorn api.main:app. Никогда не запускайте голый uvicorn api.main:app — это разрешает uvicorn из PATH, который может принадлежать другому Python, чем тот, в котором находится Feast, и ошибка всплывает на сорок кадров глубже в цепочке импорта.

  • Держите command в .mcp.json как абсолютный путь к интерпретатору. "python3" там разрешается относительно PATH, который был у хост-процесса.

Что находится в реестре

Сущности — card (card_id), customer (customer_id)

Представления признаков

Представление

Сущность

Тип

Признаки

TTL

card_velocity

card

push

txn_count_1h, txn_count_24h, amount_sum_1h

2h

customer_profile

customer

batch

avg_amount_30d, distinct_merchants_30d, home_country, chargebacks_lifetime

7d

Сервис признаков — fraud_model_v2, связывающий все 7 признаков.

Разделение TTL 2h / 7d намеренно: оно заставляет инструменты проверки свежести давать реальные ответы, а не постоянное всё-зелёное.

Тестовые данные

data_gen/generate_swipes.py записывает 15 000 снимков клиентов (500 клиентов × 30 дней) и 14 394 строки скорости (600 карт × 24 часа, минус 6 удалённых для создания случая устаревания). Всё привязано к времени выполнения, поэтому повторная генерация всегда даёт данные, которые чисто материализуются.

Шесть персон зафиксированы, чтобы демонстрации были детерминированными:

Карта / Клиент

Настройка

Демонстрирует

C-4471 / CU-8842

7 свайпов/час, $2 140 против средних $58,20, chargebacks null

Случай мошенничества и нулевой признак

C-1002 / CU-1002

Всё медианное

Контроль

C-7788 / CU-3310

Самая новая строка скорости — 6 часов назад

Устаревание за пределами TTL 2h

C-9999

Никогда не генерировалась

Неизвестная сущность

CU-5150

Профиль есть, карты нет

Частичное покрытие

C-3355 / CU-4402

4 chargebacks, нормальная скорость

Риск, который не является скоростью

API

Группа

Эндпоинты

Каталог

/entities /data-sources /feature-views /feature-views/{n} /feature-services /feature-services/{n} /features/search /cards/{id}

Происхождение

/features/{view}/{feature}/lineage /feature-views/{n}/consumers

Здоровье

/feature-views/{n}/freshness /health/materialization /feature-views/{n}/materialize ⚠

Значения

/features/online /features/explain /features/push ⚠

Интерактивная документация на http://localhost:8000/docs.

API не является прокси-передачей. Он делает три вещи, которые сырой SDK не делает: объединяет метаданные реестра с временными метками онлайн-хранилища для вычисления свежести, проходит по цепочке источник → представление → сервис для вычисления происхождения и уплощает proto-формы Feast в простые именованные объекты.

MCP-инструменты

12 инструментов только для чтения, плюс 2 инструмента записи, которые регистрируются только при включённой записи.

list_feature_views · describe_feature_view · list_feature_services · describe_feature_service · search_features · list_entities · resolve_card · get_feature_lineage · get_feature_consumers · check_feature_freshness · get_online_features · explain_features_for_entity · push_swipe ⚠ · trigger_materialization ⚠

Два вида свежести

Они отвечают на разные вопросы, и их путаница — самая опасная ошибка, доступная здесь:

Инструмент

Отвечает

Область

check_feature_freshness

«Мёртв ли конвейер?»

Все сущности, уровень представления

explain_features_for_entity

«Актуальны ли данные этой карты?»

Одна сущность

Отдельная сущность может быть на шесть часов устаревшей внутри представления, которое материализовалось секунды назад — материализация записывает то, что содержал источник, а для карты без недавних строк это старое значение. Поэтому представление, показывающее OK, ничего не доказывает о конкретной карте.

Небольшая модель стабильно путает эти два понятия и отвечает «достаточно актуально, чтобы доверять» на основе метаданных уровня представления. Три уровня защищают от этого: INSTRUCTIONS сервера, описание инструмента check_feature_freshness и примечание, добавленное к выводу этого инструмента — последнее сработало на самом деле, поскольку модель, пропустившая описание, всё равно читает результат, на который она действовала.

Зачем существует explain_features_for_entity

get_online_features возвращает голые значения. Голый null не может различить четыре разные ситуации, и Feast без жалоб выдаёт просроченное значение:

  • настоящий ноль

  • представление, которое никогда не материализовалось

  • сущность, которая не существует

  • значение, которое просрочено по TTL

explain_features_for_entity разделяет их, используя event_ts для каждой сущности, восстановленный из онлайн-хранилища. Поэтому это предпочтительный инструмент извлечения.

FEAST_MCP_READONLY

Читается обоими процессами. Если true (по умолчанию), MCP-сервер вообще не регистрирует push_swipe или trigger_materialization — инструмент, который модель не видит, она и не попытается вызвать, — а API независимо возвращает 403 на этих маршрутах, так что прямой запрос через curl тоже отклоняется.

Локальный LLM-хост

host.py — это настоящий MCP-хост, управляемый локальной open-source моделью — без API-ключа, ничего внешнего. Модель сама решает, какие инструменты вызывать; mcp_cli.py вызывает только те инструменты, которые вы назвали.

ollama/qwen2.5:7b  ->  host.py  ->  MCP server  ->  Feature API  ->  Feast  ->  SQLite
ollama serve &                      # if not already running
ollama pull qwen2.5:7b              # any tool-calling model works

python3 host.py "Why would card C-4471 be flagged?"
python3 host.py --trace --quiet "Is anything stale?"
python3 host.py                     # interactive

Системный промпт не прописан в host.py. Он приходит из собственных instructions MCP-сервера, возвращаемых во время initialize() — сервер сообщает модели, как следует использовать его инструменты, а хост просто передаёт это дальше. Изменение INSTRUCTIONS в mcp_server/server.py меняет поведение модели без правки хоста.

Выбор модели имеет значение: нужна поддержка вызова инструментов. qwen2.5:7b работает; у Gemma нет tool-шаблона в Ollama, и она не заработает.

Защитные механизмы хоста

Модель на 7B — ненадёжный планировщик, поэтому цикл защищается от трёх сбоев, которые она реально демонстрирует:

Сбой

Защитный механизм

Повторяет уже сделанный вызов, иногда до упора в лимит шагов

Результаты кэшируются по (tool, args); повтор обслуживается из кэша с пометкой «ты это уже делал» вместо второго круга запросов

Описывает следующий вызов прозой («Next, let's call describe_feature_view») вместо того, чтобы его эмитировать

Распознаётся, один раз подталкивается к эмиссии вызова, а не его описанию (макс. 2)

Уходит за бюджет шагов без ответа

На последнем шаге — или после 3 повторов — инструменты отзываются, так что модель обязана ответить по собранным данным

Каждый случай печатает строку HOST |, так что вмешательство цикла видно.

Даже так, на открытых промптах стоит ждать блужданий. Практическое решение — ограничить набор инструментов:

python3 host.py --tools resolve_card,explain_features_for_entity,check_feature_freshness \
  "Why would card C-4471 be flagged?"

Наблюдаем, как MCP вызывает API

mcp_cli.py говорит на том же stdio-протоколе, что и хост, так что цепочку MCP → API можно наблюдать из шелла:

python3 mcp_cli.py tools                    # what is registered
python3 mcp_cli.py --trace demo             # 11-step walkthrough, with HTTP calls
python3 mcp_cli.py --trace call resolve_card '{"card_id": "C-4471"}'

--trace печатает эндпоинт, который задевает каждый инструмент:

      http | HTTP Request: GET http://localhost:8000/cards/C-4471 "HTTP/1.1 200 OK"
C-4471 is owned by CU-8842

Попробуйте

Разобрать отклонение

«Почему карта C-4471 получила отказ?»

list_feature_services → resolve_card → explain_features_for_entity. Возвращает 7 свайпов за последний час на сумму $2,140 при среднем $58.20, причём история чарджбэков явно недоступна, а не принята за ноль.

Поймать мёртвый пайплайн

«Есть ли что-то устаревшее для карты C-7788?»

explain_features_for_entity помечает card_velocity как устаревший на 6ч46м при TTL в 2ч. Значения всё равно возвращаются — ничто не блокирует чтение, — и именно поэтому флаг так нужен.

Круговой прогон push (нужны включённые записи)

«Запиши свайп по C-7788, затем проверь снова.»

push_swipe → та же карта читается свежей. trigger_materialization на card_velocity сбрасывает её к строке батча 6-часовой давности, так что демо можно повторять.

Структура

requirements.txt  pinned, verified working set
preflight.py      interpreter + dependency check, run by both scripts
setup.sh          data + apply + materialize
run_api.sh        starts the API on the right interpreter
run_mcp.sh        starts the MCP server by hand (debugging)
mcp_cli.py        drives the MCP server from a shell, with --trace
host.py           local-LLM MCP host -- the model picks the tools

feature_repo/     Feast definitions + feature_store.yaml   (the only Feast config)
data_gen/         mock data generator
api/              FastAPI + the Feast SDK        <- the API boundary
  routers/        catalog | lineage | health | values
mcp_server/       MCP tools, HTTP client only    <- no Feast import
  tools/          catalog | lineage | health | values | admin

api/routers/ и mcp_server/tools/ зеркально соответствуют друг другу один-в-один.

Примечания

  • chargebacks_lifetime — это Float64, а не Int64. Фича действительно nullable, а у null-целого нет представления в пути Parquet → pandas → Feast.

  • Для настройки используйте feast materialize, а не materialize-incremental. Инкрементальный режим берёт TTL вьюхи как нижнюю границу, так что при TTL в 2ч он пропустил бы строку 6-часовой давности, на которой держится персона «устаревшие данные».

  • Кэширование реестра. cache_ttl_seconds: 30 в feature_store.yaml означает, что feast apply из другого шелла подхватывается в течение 30с. POST /admin/reload форсирует это немедленно, а также переоткрывает online store — чего голое обновление реестра не делает.

  • Мок-данные привязаны ко времени. У card_velocity TTL 2ч, так что спустя пару часов после ./setup.sh каждая карта читается устаревшей, и персонажи перестают различаться. Перезапустите ./setup.sh.

  • Конкурентность SQLite. Запись feast materialize во время чтения uvicorn может приводить к конфликтам блокировок. Локально это нормально; это не production online store.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables interaction with Tecton clusters through MCP, allowing management of feature stores, execution of Tecton CLI commands, and retrieval of feature store configurations via natural language.
    -
  • A
    license
    A
    quality
    D
    maintenance
    Exposes Azure AI Foundry agents, workflows, and AI Search vector-database capabilities as MCP tools, enabling natural language interaction with agents, semantic search, and index management.
    10
    2
    MIT