Skip to main content
Glama

mcp_feast

一个基于 Feast 特征存储的 MCP 服务器,用于信用卡刷卡欺诈模型。完全本地运行: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 还可以服务第二个消费者——一个模型服务器——它虽然不在此项目中构建,但会像 MCP 层一样调用 POST /features/online

Related MCP server: tecton-mcp

实际运行的内容

进程

启动方式

持有内容

端口

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 通过 provider 的 online_read 恢复每个实体的 event_timestamp——这与 get_online_features 内部调用的相同,但会暴露时间戳——并将其与视图的 TTL 进行比对,从而将它们区分开来。

原始 SDK 不会给你的三个事实:

端点

推导出的内容

/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

物化写入的是源持有的任何内容。对于没有近期行的卡片,那就是一个旧值——所以一个实体可能在一个几秒前才物化的视图内已经过期数小时。刷新视图无法修复它;只有推送才能。

问题

工具

范围

"管道死了吗?"

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/ 逐文件镜像——catalog、lineage、health、values——所以导航一目了然。

两个支撑设计的思想

错误被写成指令。 404 返回 Available: [...],错误的实体行会指出它需要的连接键。反复观察到:7B 模型会出错,读取错误信息,然后在下一步自我修正,而不是再次猜测。

指导附着在输出上,而不仅仅是描述上。 工具描述会被跳过;结果不会。新鲜度范围警告和 trigger_materialization 的"调用 check_feature_freshness 确认"都位于返回文本中,当仅靠提示措辞失败时,两者都改变了模型行为。


快速开始

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,而不是本项目使用的 2.x mcp.server.MCPServer。shell 提示符显示激活的 conda 环境并不能证明什么——检查 PATH:

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

如果框架或系统 Python 排在你的环境之前,那么无论提示符显示什么,每次 python3 调用都会逃出环境。用以下方式正确诊断:

python3 preflight.py

它导入的是代码各部分需要的精确符号——而不仅仅是模块——所以错误主版本的依赖会被按名称捕获,并且当 PATH 上的 uvicornfeast 属于不同环境时它会发出警告。

每个入口点都接受 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

两条规则可以完全避免这个问题:

  • ./run_api.shpython3 -m uvicorn api.main:app 启动 API。绝不要裸用 uvicorn api.main:app——那会从 PATH 解析 uvicorn,它可能属于持有 Feast 之外的不同 Python,失败会在导入链四十层深处才浮现。

  • 保持 .mcp.jsoncommand 为绝对解释器路径。那里的 "python3" 会根据宿主进程恰好拥有的 PATH 来解析。

注册表里有什么

实体cardcard_id)、customercustomer_id

特征视图

视图

实体

类型

特征

TTL

card_velocity

card

push

txn_count_1htxn_count_24hamount_sum_1h

2h

customer_profile

customer

batch

avg_amount_30ddistinct_merchants_30dhome_countrychargebacks_lifetime

7d

特征服务fraud_model_v2,绑定全部 7 个特征。

2h / 7d 的 TTL 划分是刻意的:它让新鲜度工具产生真实答案,而不是永远的全绿。

模拟数据

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 小时

超过 2h TTL 的过期

C-9999

从未生成

未知实体

CU-5150

有画像但没有卡片

部分覆盖

C-3355 / CU-4402

4 次 chargebacks,正常速度

不是速度的风险

API

分组

端点

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

交互式文档位于 http://localhost:8000/docs

API 不是直通层。它做了原始 SDK 不会做的三件事:将注册表元数据与在线存储时间戳连接以计算新鲜度,遍历源 → 视图 → 服务以计算血缘,以及将 Feast 的 proto 形状扁平化为普通的命名对象。

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 的视图不能证明任何特定卡片的情况。

小模型确实会可靠地混淆这两者,并从视图级元数据回答"足够新可以信任"。有三层防护:服务器 INSTRUCTIONScheck_feature_freshness 工具描述,以及附加到该工具输出上的注释——最后一个是真正起作用的,因为跳过描述的模型仍然会读取它操作的结果。

为什么存在 explain_features_for_entity

get_online_features 返回裸值。一个裸 null 无法区分四种不同的情况,而且 Feast 会毫无怨言地提供过期值:

  • 真正的零

  • 从未物化的视图

  • 不存在的实体

  • 超过 TTL 的值

explain_features_for_entity 使用从在线存储恢复的每个实体的 event_ts 将它们区分开来。这就是为什么它是首选的检索工具。

FEAST_MCP_READONLY

两个进程都会读取。当为 true(默认值)时,MCP 服务器根本不会注册 push_swipetrigger_materialization —— 模型看不到的工具它就不会尝试调用 —— 并且 API 会独立地对这些路由返回 403,因此直接 curl 也会被拒绝。

本地 LLM 主机

host.py 是一个由本地开源模型驱动的真实 MCP 主机 —— 无需 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 中。它来自 MCP 服务器自身的 instructions,在 initialize() 期间返回 —— 服务器告诉模型其工具应如何使用,主机则原样传递。修改 mcp_server/server.py 中的 INSTRUCTIONS 即可改变模型的行为,无需编辑主机。

模型的选择很重要:它需要支持工具调用。qwen2.5:7b 可用;Gemma 在 Ollama 中没有工具模板,因此无法使用。

主机护栏

7B 模型是一个不可靠的规划器,因此循环针对它实际表现出的三种失败模式进行防御:

失败模式

护栏

重复调用已经执行过的调用,有时一直重复到步数上限

按(工具,参数)缓存结果;重复调用会从缓存中返回,并附带"你已经做过这个"的提示,而不是再次往返

用散文叙述下一步调用("接下来,让我们调用 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 链路可以从 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 会打印每个工具命中的端点:

      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_servicesresolve_cardexplain_features_for_entity。 返回过去一小时内 7 笔刷卡,总计 $2,140,而平均值为 $58.20,并且退单历史明确显示为不可用,而不是假定为零。

发现管道停滞

"卡片 C-7788 有什么数据过期了吗?"

explain_features_for_entitycard_velocity 标记为已过期 6 小时 46 分钟,而 TTL 为 2 小时。数值仍然会返回 —— 没有任何东西阻止读取 —— 这正是需要该标记的原因。

推送往返(需要启用写入)

"记录一笔 C-7788 的刷卡,然后再查一次。"

push_swipe → 同一张卡片读取到最新数据。对 card_velocity 执行 trigger_materialization 会将其重置为 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_lifetimeFloat64,不是 Int64 该特征确实可以为空,而空整数在 Parquet → pandas → Feast 链路中没有表示方式。

  • 设置时使用 feast materialize,而不是 materialize-incremental 增量模式使用视图的 TTL 作为起始边界,因此使用 2 小时 TTL 时会跳过那条 6 小时前的行,而正是那行数据让"过期"角色得以成立。

  • 注册表缓存。 feature_store.yaml 中的 cache_ttl_seconds: 30 意味着在另一个 shell 中执行 feast apply 会在 30 秒内生效。POST /admin/reload 会立即强制刷新,并且还会重新打开在线存储 —— 这是单纯的注册表刷新不会做的。

  • 模拟数据是时间锚定的。 card_velocity 的 TTL 为 2 小时,因此在 ./setup.sh 运行几个小时后,每张卡片都会读取为过期状态,各角色之间不再可区分。重新运行 ./setup.sh

  • SQLite 并发。 feast materialize 在写入时,uvicorn 同时在读取,可能会遇到锁竞争。本地使用没问题;它不是生产环境的在线存储。

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI models to interact with local CSV and Parquet data through MCP tools, providing summarization and analysis capabilities.
    1
  • 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

View all related MCP servers

Related MCP Connectors

  • Hosted MCP endpoint with realistic fake data for prototyping agents. 12 tools, no setup.

  • Shared, governed long-term memory for AI agents across tools and sessions via MCP and REST.

  • A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sidbu546/mcp_feast_dev'

If you have feedback or need assistance with the MCP directory API, please join our Discord server