feast-mcp
README.md
# Feast MCP Server
Standalone MCP server for Feast, built on [FastMCP](https://gofastmcp.com/).
Composes two namespaced sub-servers behind a single MCP endpoint:
- **features** — proxies to the Feast feature server (online features, vector search, push, materialization). Mounted when `--feast-url` is provided.
- **registry** — proxies to the Feast REST registry server (browse feature views, entities, data sources, lineage). Mounted when `--registry-url` is provided.
The caller's `Authorization: Bearer <token>` header is passed through so that upstream servers handle OIDC / Kubernetes RBAC as usual.
## Documentation
Full guides live in [`docs/`](docs/README.md):
- [Configuration](docs/configuration.md) — every setting and how to set it
- [Deployment](docs/deployment.md) — running locally, with Docker, and on Kubernetes
- [Development](docs/development.md) — changing the code, tests, and adding tools
## Quick start
```bash
cd MCP
pip install -e .
# Feature server only (9 tools)
feast-mcp --feast-url http://localhost:6566
# Registry only (13 tools)
feast-mcp --registry-url http://localhost:8080
# Both (22 tools)
feast-mcp --feast-url http://localhost:6566 --registry-url http://localhost:8080
# HTTP transport on a custom port
feast-mcp --feast-url http://localhost:6566 --transport http --port 8000
```
Environment variables work too:
```bash
export FEAST_MCP_FEATURE_SERVER_URL=http://localhost:6566
export FEAST_MCP_REGISTRY_URL=http://localhost:8080
feast-mcp
```
## Tools exposed
### Feature server tools (`features_` prefix)
| Tool | Upstream endpoint | Description |
|---|---|---|
| `features_get_online_features` | `POST /get-online-features` | Retrieve online feature values |
| `features_search` | `POST /search` | Vector similarity search |
| `features_list_vector_stores` | `GET /v1/vector_stores` | List vector stores |
| `features_get_vector_store` | `GET /v1/vector_stores/{id}` | Get a vector store |
| `features_vector_store_search` | `POST /v1/vector_stores/{id}/search` | OpenAI-compatible vector search |
| `features_push` | `POST /push` | Push features to the store |
| `features_materialize` | `POST /materialize` | Materialize features |
| `features_materialize_incremental` | `POST /materialize-incremental` | Incremental materialization |
| `features_health` | `GET /health` | Health check |
### Registry tools (`registry_` prefix)
| Tool | Upstream endpoint | Description |
|---|---|---|
| `registry_list_projects` | `GET /api/v1/projects` | List all projects |
| `registry_get_project` | `GET /api/v1/projects/{name}` | Get project details |
| `registry_list_entities` | `GET /api/v1/entities` | List entities in a project |
| `registry_get_entity` | `GET /api/v1/entities/{name}` | Get entity details |
| `registry_list_feature_views` | `GET /api/v1/feature_views` | List all feature views |
| `registry_get_feature_view` | `GET /api/v1/feature_views/{name}` | Get feature view details |
| `registry_list_features` | `GET /api/v1/features` | List individual features |
| `registry_list_feature_services` | `GET /api/v1/feature_services` | List feature services |
| `registry_get_feature_service` | `GET /api/v1/feature_services/{name}` | Get feature service details |
| `registry_list_data_sources` | `GET /api/v1/data_sources` | List data sources |
| `registry_get_data_source` | `GET /api/v1/data_sources/{name}` | Get data source details |
| `registry_search_registry` | `GET /api/v1/search` | Full-text search across registry |
| `registry_get_lineage` | `GET /api/v1/lineage/complete` | Get lineage relationships |
## Project structure
```
feast_mcp/
server.py — composition + CLI entry point
features.py — feature server tools (9 tools)
registry.py — registry server tools (13 tools)
client.py — shared HTTP client
auth.py — shared auth helpers
session_storage/ — factory for the shared OAuth-state store (key_value.aio)
observability/ — logging fanned out to stdout + OpenTelemetry
```
## Authorization
The MCP server itself does **not** validate tokens. It forwards the bearer token from the MCP request context to the upstream servers, which perform OIDC or Kubernetes token validation and RBAC enforcement.
For OIDC browser-based login:
```bash
feast-mcp \
--feast-url http://localhost:6566 \
--auth-mode oidc \
--oidc-discovery-url https://keycloak.example.com/realms/feast/.well-known/openid-configuration \
--oidc-client-id feast-mcp \
--transport http --port 8000
```
### Session storage (load-balanced OIDC)
The OIDC OAuth flow is a multi-request handshake (`/authorize` → IdP →
`/callback` → `/token`), and the proxy correlates those requests through
server-side state: client registrations, in-flight transactions,
authorization codes, and token mappings. By default this state lives in an
**on-disk, per-node** store, so behind a load balancer with more than one
replica a `/callback` can hit a replica that never saw the matching
`/authorize` — and the login fails.
Point the proxy at a **shared** backend to make the flow load-balancer safe.
Backends are provided by [`py-key-value-aio`](https://pypi.org/project/py-key-value-aio/):
`redis`, `valkey`, `postgresql`, `mongodb`, `disk`, `memory`.
```bash
feast-mcp \
--feast-url http://localhost:6566 \
--auth-mode oidc \
--oidc-discovery-url https://keycloak.example.com/realms/feast/.well-known/openid-configuration \
--oidc-client-id feast-mcp \
--session-storage-backend redis \
--transport http --port 8000
```
The backend can also be set via `FEAST_MCP_SESSION_STORAGE_BACKEND`.
Backend-specific connection options go in `feast_mcp.yaml` under
`session_storage.options` and are passed through to the underlying store:
```yaml
session_storage:
backend: redis # redis | valkey | postgresql | mongodb | disk | memory
options:
url: redis://localhost:6379
```
Install the extra for your chosen backend, e.g.
`pip install 'py-key-value-aio[redis]'`. When no backend is configured, the
proxy falls back to FastMCP's default on-disk store (fine for a single
replica). `memory` and `disk` are **not** shared across processes — the
server logs a warning if you select them.
## Observability (logging + OpenTelemetry)
Logs always go to the console (stderr — stdout is reserved by the MCP stdio
transport). When an OTLP endpoint is configured, the **same** log lines are
*also* exported to your OpenTelemetry backend for visibility.
FastMCP, the MCP SDK, and the web server (uvicorn / gunicorn) log to their own
logger trees. Those are **bridged** onto the same handlers, so their output
lands on the console *and* in OpenTelemetry alongside the server's own logs —
you don't lose framework logs.
Every tool call passes through `get_auth_token()`, which logs one line of auth
context per request: the authenticated **user** (from the token claims —
`preferred_username`/`email`/`sub` and `client_id`), the client **IP**
(honoring `X-Forwarded-For` / `X-Real-IP` behind a proxy), and the **request**
(`METHOD /path`). Unauthenticated requests are logged too. Example:
```
INFO feast_mcp.auth [trace=4bf92f3577b34da6a3ce929d0e0e4736] Authenticated request: user=alice (client_id=feast-mcp) ip=10.0.0.7 request=POST /mcp
```
**Request correlation (trace ids).** Each incoming HTTP request is wrapped in
an OpenTelemetry span, so *every* log line emitted while handling that request
— tool dispatch, the auth line above, upstream calls — carries the **same**
`trace_id`. This lets you group "all logs for one request" in your backend
(`trace_id` field in JSON logs, `[trace=…]` in text logs). When the OTEL SDK is
installed the id is a real W3C trace id and the span itself is exported too, so
logs and traces line up and you can jump between them. Without the SDK, a
generated per-request id is used so console logs are still groupable.
OTEL export is optional — install the extra:
```bash
pip install 'feast-mcp[otel]'
```
Enable it by pointing at an OTLP collector:
```bash
feast-mcp \
--feast-url http://localhost:6566 \
--log-level INFO \
--log-format json \
--otel-endpoint http://localhost:4317 \
--transport http --port 8000
```
Setting `--otel-endpoint` turns export on automatically. Configuration
resolves from CLI args, then environment variables, then `feast_mcp.yaml`:
| Setting | CLI | Env var | Default |
|---|---|---|---|
| Log level | `--log-level` | `FEAST_MCP_LOG_LEVEL` | `INFO` |
| Log format (`text`/`json`) | `--log-format` | `FEAST_MCP_LOG_FORMAT` | `text` |
| Console logging | — | `FEAST_MCP_LOG_STDIO` | `true` |
| OTLP endpoint | `--otel-endpoint` | `FEAST_MCP_OTEL_ENDPOINT` / `OTEL_EXPORTER_OTLP_ENDPOINT` | — |
| Protocol (`grpc`/`http`) | `--otel-protocol` | `FEAST_MCP_OTEL_PROTOCOL` / `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` |
| Service name | `--otel-service-name` | `FEAST_MCP_OTEL_SERVICE_NAME` / `OTEL_SERVICE_NAME` | `feast-mcp` |
| OTLP headers | — | `FEAST_MCP_OTEL_HEADERS` / `OTEL_EXPORTER_OTLP_HEADERS` | — |
Standard `OTEL_*` variables are honored as a fallback, so existing
OpenTelemetry tooling works unchanged. Equivalent `feast_mcp.yaml`:
```yaml
observability:
level: INFO
format: json
stdio: true
otel_endpoint: http://localhost:4317
otel_protocol: grpc # grpc | http
otel_service_name: feast-mcp
```
If OTEL is requested but the SDK/exporter isn't installed, the server logs a
warning and continues with console logging only.
See [`examples/otel_demo`](examples/otel_demo/README.md) for a runnable setup
with a local OpenTelemetry Collector that prints the exported log records.
## IDE / client configuration
### Local (client launches `feast-mcp` over stdio)
The client starts the server as a subprocess and talks to it over stdio — no
port, no running server to manage:
```json
{
"mcpServers": {
"feast": {
"command": "feast-mcp",
"args": [
"--feast-url", "http://localhost:6566",
"--registry-url", "http://localhost:8080"
]
}
}
}
```
Feature server only:
```json
{
"mcpServers": {
"feast": {
"command": "feast-mcp",
"args": ["--feast-url", "http://localhost:6566"]
}
}
}
```
Registry only:
```json
{
"mcpServers": {
"feast": {
"command": "feast-mcp",
"args": ["--registry-url", "http://localhost:8080"]
}
}
}
```
### Remote (paste a URL — no local command)
When the server is already running over HTTP (started with `--transport http`
or deployed behind a load balancer), the client doesn't launch anything — just
point it at the URL. Nothing is installed locally.
The endpoint depends on the transport the server was started with:
| Server transport | Endpoint to paste |
|---|---|
| `http` / `streamable-http` | `http://<host>:<port>/mcp` |
| `sse` | `http://<host>:<port>/sse` |
```json
{
"mcpServers": {
"feast": {
"url": "http://localhost:8000/mcp"
}
}
}
```
For a deployed server, use its public URL (HTTPS recommended):
```json
{
"mcpServers": {
"feast": {
"url": "https://feast-mcp.example.com/mcp"
}
}
}
```
If the server runs with `--auth-mode oidc`, the client performs the browser
login automatically on first connect. For a server that expects a bearer
token directly, clients that support custom headers can send one:
```json
{
"mcpServers": {
"feast": {
"url": "https://feast-mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer <token>"
}
}
}
}
```
> Some clients name the fields differently (e.g. `"type": "http"` /
> `"transport": "sse"`). Check your client's MCP docs if `url` alone isn't
> recognized.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues