mcp_feast

mcp_feast

Enables LLMs to inspect and interact with a local Feast feature store through MCP tools, including listing feature views and services, resolving entities, checking freshness, explaining feature values, and optionally pushing swipes or triggering materialization.

Category
访问服务器

README

mcp_feast

An MCP server over a Feast feature store, for a card-swipe fraud model. Runs entirely locally: Parquet offline store, SQLite online store, no cloud, no broker.


System design

The four layers

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

That thick arrow is the whole design. Everything Feast-specific lives below it. The MCP server above it needs no Feast install, no store drivers, and no warehouse credentials — it is an HTTP client and nothing more.

That buys three things. Swapping SQLite for Redis becomes a feature_store.yaml change the MCP layer never sees. A laptop running the MCP server needs one reachable URL instead of a network route to production Redis. And the same API can serve a second consumer — a model server — that was never built here but would call POST /features/online exactly as the MCP layer does.

What actually runs

Process Started by Holds Port
Feature API ./run_api.sh the FeatureStore singleton 8000
MCP server the host, over stdio an httpx client —
Ollama ollama serve qwen2.5:7b 11434
Host python3 host.py the conversation loop —

Only the API imports Feast. Verify it:

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

One request, end to end

Asking "why would card C-4471 be flagged?" crosses every layer twice:

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

That second SDK call is the part Feast does not give you for free — see below.

How data reaches the online store

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

The dashed path is the training half of a feature store. It is left out on purpose: it runs a minutes-long query returning millions of rows, which is the wrong shape for a chat tool. That is also why the generator writes no fraud labels.

Why the API is not a passthrough

get_online_features() returns values and nothing else. A bare null cannot tell you which of four situations you are in — and Feast serves an expired value without complaint:

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 separates them by recovering the per-entity event_timestamp through the provider's online_read — the same call get_online_features makes internally, but one that surfaces the timestamp — and joining it against the view's TTL.

Three facts the raw SDK will not give you:

Endpoint Derives
/features/explain per-feature freshness and missing-value reason
/features/{view}/{feature}/lineage source → view → consuming services
/feature-views/{name}/consumers blast radius before a change

The trap this is built to avoid

Freshness is per entity, not per view. Both are real questions with different answers, and confusing them is the most dangerous mistake available here:

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

Materialization writes whatever the source holds. For a card with no recent rows that is an old value — so an entity can be hours stale inside a view that materialized seconds ago. Refreshing the view cannot fix it; only a push can.

Question Tool Scope
"Is a pipeline dead?" check_feature_freshness all entities
"Is this card current?" explain_features_for_entity one entity

A small model reliably conflates these. What fixed it was not the system prompt — it was appending the warning to check_feature_freshness's output. A model that skips a tool description still reads the result it just acted on.

Tools map to endpoints one to one

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/ and mcp_server/tools/ mirror each other file for file — catalog, lineage, health, values — so navigation is obvious.

Two ideas that carried the design

Errors are written as instructions. A 404 returns Available: [...], and a bad entity row names the join keys it needs. Observed repeatedly: a 7B model gets it wrong, reads the error, and fixes itself on the next step rather than guessing again.

Guidance rides on output, not just descriptions. Tool descriptions get skipped; results do not. Both the freshness scope warning and trigger_materialization's "call check_feature_freshness to confirm" live in the returned text, and both changed model behaviour when prompt wording alone had failed.


Quick start

Python 3.11. Feast declares >=3.10 but classifies only 3.10, and its transitive stack is the usual source of trouble on newer interpreters.

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

Both scripts honour a PYTHON override if the deps live elsewhere:

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

The MCP server is launched by the host via .mcp.json, which pins an absolute interpreter path for the reason in Troubleshooting below. ./run_mcp.sh runs it by hand for debugging.

Troubleshooting: wrong interpreter

Two symptoms, one cause -- a different Python than the one holding the deps:

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

The second is the sneakier one: mcp 1.x imports fine but exposes mcp.server.fastmcp.FastMCP, not the 2.x mcp.server.MCPServer this project uses. A shell prompt showing an active conda env is not proof -- check PATH:

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

If a framework or system Python sits ahead of your env, every python3 call escapes the env regardless of what the prompt says. Diagnose properly with:

python3 preflight.py

It imports the exact symbol each part of the code needs -- not just the module -- so a wrong-major dependency is caught by name, and it warns when uvicorn or feast on your PATH belong to a different environment.

Every entry point takes a PYTHON override, so you never have to fight 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

Two rules avoid this entirely:

  • Start the API with ./run_api.sh, or python3 -m uvicorn api.main:app. Never bare uvicorn api.main:app — that resolves uvicorn from PATH, which may belong to a different Python than the one holding Feast, and the failure surfaces forty frames deep in an import chain.
  • Keep .mcp.json's command an absolute interpreter path. "python3" there resolves against whatever PATH the host process happened to have.

What is in the registry

Entities — card (card_id), customer (customer_id)

Feature views

View Entity Kind Features 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

Feature service — fraud_model_v2, binding all 7 features.

The 2h / 7d TTL split is deliberate: it makes the freshness tooling produce real answers instead of a permanent all-green.

Mock data

data_gen/generate_swipes.py writes 15,000 customer snapshots (500 customers × 30 days) and 14,394 velocity rows (600 cards × 24 hours, minus 6 dropped to create the stale case). Everything is anchored to run time, so regenerating always produces data that materializes cleanly.

Six personas are pinned so demos are deterministic:

Card / Customer Setup Demonstrates
C-4471 / CU-8842 7 swipes/hr, $2,140 vs $58.20 average, chargebacks null The fraud case, and a null feature
C-1002 / CU-1002 Everything median Control
C-7788 / CU-3310 Newest velocity row is 6h old Staleness past a 2h TTL
C-9999 Never generated Unknown entity
CU-5150 Profile but no card Partial coverage
C-3355 / CU-4402 4 chargebacks, normal velocity Risk that isn't velocity

The API

Group Endpoints
Catalog /entities /data-sources /feature-views /feature-views/{n} /feature-services /feature-services/{n} /features/search /cards/{id}
Lineage /features/{view}/{feature}/lineage /feature-views/{n}/consumers
Health /feature-views/{n}/freshness /health/materialization /feature-views/{n}/materialize ⚠
Values /features/online /features/explain /features/push ⚠

Interactive docs at http://localhost:8000/docs.

The API is not a passthrough. It does three things the raw SDK does not: joins registry metadata against online-store timestamps to compute freshness, walks source → view → service to compute lineage, and flattens Feast's proto shapes into plain named objects.

MCP tools

12 read-only tools, plus 2 write tools that only register when writes are enabled.

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 ⚠

Two kinds of freshness

These answer different questions, and confusing them is the most dangerous mistake available here:

Tool Answers Scope
check_feature_freshness "Is a pipeline dead?" All entities, view level
explain_features_for_entity "Is this card's data current?" One entity

An individual entity can be six hours stale inside a view that materialized seconds ago -- materialization writes whatever the source held, and for a card with no recent rows that is an old value. So a view showing OK proves nothing about any particular card.

A small model reliably conflates the two and answers "current enough to trust" from view-level metadata. Three layers guard against it: the server INSTRUCTIONS, the check_feature_freshness tool description, and a note appended to that tool's output -- the last being the one that actually worked, since a model that skipped the description still reads the result it acted on.

Why explain_features_for_entity exists

get_online_features returns bare values. A bare null cannot distinguish four different situations, and Feast serves an expired value without complaint:

  • a genuine zero
  • a view that was never materialized
  • an entity that does not exist
  • a value that is past its TTL

explain_features_for_entity separates them, using the per-entity event_ts recovered from the online store. That is why it is the preferred retrieval tool.

FEAST_MCP_READONLY

Read by both processes. When true (the default), the MCP server does not register push_swipe or trigger_materialization at all — a tool the model cannot see is one it will not try — and the API independently returns 403 on those routes, so curling it directly is also refused.

Local LLM host

host.py is a real MCP host driven by a local open-source model -- no API key, nothing hosted. The model decides which tools to call; mcp_cli.py only calls tools you name.

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

The system prompt is not written in host.py. It comes from the MCP server's own instructions, returned during initialize() -- the server tells the model how its tools are meant to be used, and the host passes that through. Changing INSTRUCTIONS in mcp_server/server.py changes how the model behaves, with no edit to the host.

Model choice matters: it needs tool-calling support. qwen2.5:7b works; Gemma has no tool template in Ollama and will not.

Host guardrails

A 7B model is an unreliable planner, so the loop defends against three failures it actually exhibits:

Failure Guardrail
Repeats a call it already made, sometimes until the step limit Results cached by (tool, args); a repeat is served from cache with a "you already did this" note instead of a second round trip
Narrates its next call in prose ("Next, let's call describe_feature_view") instead of emitting one Detected, nudged once to emit the call rather than describe it (max 2)
Wanders past the step budget with no answer On the last step -- or after 3 repeats -- tools are withdrawn, so it must answer from what it gathered

Each prints a HOST | line, so you can see the loop intervening.

Even so, expect wandering on open-ended prompts. Restricting the toolset is the practical fix:

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

Watching MCP call the API

mcp_cli.py speaks the same stdio protocol the host does, so the MCP -> API chain is observable from a shell:

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 prints the endpoint each tool hits:

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

Try it

Debug a decline

"Why would card C-4471 get declined?"

list_feature_services → resolve_card → explain_features_for_entity. Returns 7 swipes in the last hour totalling $2,140 against a $58.20 average, with chargeback history explicitly unavailable rather than assumed zero.

Catch a dead pipeline

"Is anything stale for card C-7788?"

explain_features_for_entity flags card_velocity as 6h46m old against a 2h TTL. The values still come back — nothing blocks the read — which is exactly why the flag is needed.

Push round trip (needs writes enabled)

"Record a swipe on C-7788, then check it again."

push_swipe → the same card reads fresh. trigger_materialization on card_velocity resets it to the 6h-old batch row, so the demo is repeatable.

Layout

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/ and mcp_server/tools/ mirror each other one-to-one.

Notes

  • chargebacks_lifetime is Float64, not Int64. The feature is genuinely nullable, and a null integer has no representation in the Parquet → pandas → Feast path.
  • Use feast materialize, not materialize-incremental, for setup. Incremental uses the view's TTL as its start bound, so with a 2h TTL it would skip the 6h-old row that makes the stale persona work.
  • Registry caching. cache_ttl_seconds: 30 in feature_store.yaml means a feast apply in another shell shows up within 30s. POST /admin/reload forces it immediately, and also reopens the online store — which a bare registry refresh does not do.
  • The mock data is time-anchored. card_velocity has a 2h TTL, so more than a couple of hours after ./setup.sh every card reads stale and the personas stop being distinguishable. Re-run ./setup.sh.
  • SQLite concurrency. feast materialize writing while uvicorn reads can hit lock contention. Fine locally; it is not a production online store.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选