Skip to main content
Glama

mcp_feast

Feast フィーチャーストア上の MCP サーバーで、カードスワイプ不正検知モデル向けです。完全にローカルで動作します: Parquet オフラインストア、SQLite オンラインストア、クラウドなし、ブローカーなし。


システム設計

4つのレイヤー

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 クライアントにすぎません。

これにより3つのことが得られます。SQLite を Redis に置き換えることは feature_store.yaml の変更で済み、MCP レイヤーはそれを見ることはありません。MCP サーバーを実行するラップトップは、本番 Redis へのネットワークルートではなく、到達可能な URL が1つあればよいのです。また、同じ API は2番目のコンシューマー、つまりここでは構築されなかったが MCP レイヤーとまったく同じように POST /features/online を呼び出すであろうモデルサーバーにもサービスを提供できます。

Related MCP server: AI Developer Workspace MCP Server

実際に実行されるもの

プロセス

起動元

保持するもの

ポート

Feature API

./run_api.sh

FeatureStore シングルトン

8000

MCP サーバー

ホスト (stdio 経由)

httpx クライアント

—

Ollama

ollama serve

qwen2.5:7b

11434

ホスト

python3 host.py

会話ループ

—

Feast をインポートするのは API だけです。確認:

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

1リクエストのエンドツーエンド

"カード C-4471 がフラグされたのはなぜか" という質問は、すべてのレイヤーを2回横断します:

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

その2番目の 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 は、4つの状況のどれにいるかを教えてくれません。そして 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 は、プロバイダーの online_read を通じてエンティティごとの event_timestamp を回復し、それをビューの TTL と結合することで、それらを分離します。これは get_online_features が内部的に行うのと同じ呼び出しですが、タイムスタンプを表面化するものです。

生の SDK が提供しない3つの事実:

エンドポイント

導出するもの

/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

1エンティティ

小さなモデルはこれらを確実に混同します。それを修正したのはシステムプロンプトではなく、check_feature_freshness の出力に警告を追加したことでした。ツールの説明をスキップするモデルでも、自分が操作した結果は読み取ります。

ツールはエンドポイントに1対1で対応

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/ はファイルごとに互いを反映しています — カタログ、リネージ、ヘルス、バリュー — そのためナビゲーションは明白です。

設計を支えた2つのアイデア

エラーは指示として書かれます。 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 はデバッグ用に手動で実行します。

トラブルシューティング: 間違ったインタープリター

2つの症状、1つの原因 — 依存関係を保持している Python とは異なる Python:

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

2番目はより巧妙です: mcp 1.x は正常にインポートされますが、このプロジェクトが使用する 2.x の mcp.server.MCPServer ではなく mcp.server.fastmcp.FastMCP を公開します。アクティブな conda 環境を示すシェルプロンプトは証拠ではありません — PATH を確認してください:

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

フレームワークまたはシステム Python が環境の前にあれば、プロンプトが何を言おうと、すべての python3 呼び出しは環境から逃げます。適切に診断するには:

python3 preflight.py

これは、コードの各部分が必要とする正確なシンボルをインポートします — モジュールだけでなく — そのため、メジャーバージョンが間違った依存関係は名前で捕捉され、PATH 上の uvicorn または feast が別の環境に属している場合に警告します。

すべてのエントリポイントは 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

これを完全に回避する2つのルール:

  • API は ./run_api.sh または python3 -m uvicorn api.main:app で起動します。裸の uvicorn api.main:app は決して使わないでください — それは PATH から uvicorn を解決し、Feast を保持している Python とは異なる Python に属する可能性があり、失敗はインポートチェーンの40フレーム深くで表面化します。

  • .mcp.json の command は絶対インタープリターパスに保ちます。そこでの "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フィーチャーをバインド。

2h / 7d の TTL 分割は意図的です: 鮮度ツールが恒久的なオールグリーンではなく実際の答えを生成するようにします。

モックデータ

data_gen/generate_swipes.py は 15,000 件の顧客スナップショット (500 顧客 × 30 日) と 14,394 行の速度データ (600 カード × 24 時間、ただし古いケースを作るために6行削除) を書き込みます。すべて実行時間に固定されているため、再生成すると常にクリーンにマテリアライズされるデータが生成されます。

デモが決定的になるように6つのペルソナが固定されています:

カード / 顧客

セットアップ

デモンストレーション

C-4471 / CU-8842

7 スワイプ/時、$2,140 対 平均 $58.20、チャージバック null

不正ケース、および null フィーチャー

C-1002 / CU-1002

すべて中央値

コントロール

C-7788 / CU-3310

最新の速度行が6時間前

2h TTL を超えた古さ

C-9999

生成されていない

未知のエンティティ

CU-5150

プロフィールはあるがカードなし

部分的なカバレッジ

C-3355 / CU-4402

チャージバック4件、通常の速度

速度ではないリスク

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 が行わない3つのことを行います: レジストリメタデータをオンラインストアのタイムスタンプと結合して鮮度を計算し、ソース → ビュー → サービスを辿ってリネージを計算し、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 ⚠

2種類の鮮度

これらは異なる質問に答えます。混同することはここで可能な最も危険な間違いです:

ツール

答える質問

スコープ

check_feature_freshness

"パイプラインは死んでいるか?"

全エンティティ、ビューレベル

explain_features_for_entity

"このカードのデータは最新か?"

1エンティティ

個々のエンティティは、数秒前にマテリアライズされたビュー内で6時間古くなることがあります。マテリアライゼーションはソースが保持するものを書き込み、最近の行がないカードの場合、それは古い値です。したがって、ビューが OK を示しても、特定のカードについては何も証明されません。

小さなモデルは2つを確実に混同し、ビューレベルのメタデータから "信頼するのに十分最新" と答えます。3つのレイヤーがそれを防ぎます: サーバーの INSTRUCTIONS、check_feature_freshness ツールの説明、そしてそのツールの出力に追加されたメモ — 最後のものが実際に機能したもので、説明をスキップしたモデルでも自分が操作した結果は読み取るからです。

explain_features_for_entity が存在する理由

get_online_features は裸の値を返します。裸の null は4つの異なる状況を区別できず、Feast は期限切れの値を文句なしに提供します:

  • 本当のゼロ

  • マテリアライズされたことがないビュー

  • 存在しないエンティティ

  • TTL を過ぎた値

explain_features_for_entity は、オンラインストアから回復したエンティティごとの event_ts を使用してそれらを分離します。それが推奨される取得ツールである理由です。

FEAST_MCP_READONLY

両方のプロセスから読み取られます。true(デフォルト)の場合、MCPサーバーはpush_swipeやtrigger_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モデルは信頼できないプランナーであるため、ループは実際に示す3つの失敗に対して防御します:

失敗

ガードレール

すでに行った呼び出しを繰り返し、時にはステップ制限に達するまで繰り返す

結果は(tool, args)でキャッシュされ、繰り返しは2回目のラウンドトリップの代わりに「これはすでに実行済みです」というメモ付きでキャッシュから提供される

次の呼び出しを散文で語る("Next, let's call describe_feature_view")代わりに実際に発行しない

検出され、説明する代わりに呼び出しを発行するよう1回促される(最大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。 過去1時間の7回のスワイプ、合計$2,140(平均$58.20)を返し、チャージバック履歴はゼロと仮定されるのではなく、明示的に利用不可として返されます。

停止したパイプラインを検出

「カードC-7788で古くなっているものはありますか?」

explain_features_for_entityはcard_velocityが2時間のTTLに対して6時間46分古いことをフラグします。値は依然として返されます。読み取りをブロックするものはありません。これはまさにフラグが必要な理由です。

プッシュのラウンドトリップ(書き込み有効化が必要)

「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/は1対1で対応しています。

メモ

  • chargebacks_lifetimeはInt64ではなくFloat64です。 このフィーチャーは実際にnull許容であり、nullの整数はParquet → pandas → Feastパスで表現できません。

  • セットアップにはmaterialize-incrementalではなくfeast materializeを使用してください。 インクリメンタルはビューのTTLを開始境界として使用するため、2時間のTTLでは、古いペルソナを機能させる6時間前の行をスキップしてしまいます。

  • レジストリキャッシュ。 feature_store.yamlのcache_ttl_seconds: 30は、別のシェルでのfeast applyが30秒以内に反映されることを意味します。POST /admin/reloadは即座に強制し、オンラインストアも再オープンします。これは単純なレジストリリフレッシュでは行われません。

  • モックデータは時間に固定されています。 card_velocityには2時間のTTLがあるため、./setup.shの実行から数時間以上経過すると、すべてのカードが古い状態で読み取られ、ペルソナを区別できなくなります。./setup.shを再実行してください。

  • SQLiteの並行性。 uvicornが読み取っている間にfeast materializeが書き込むと、ロック競合が発生する可能性があります。ローカルでは問題ありません。本番のオンラインストアではありません。

Related MCP Connectors

Related MCP Servers