Packages the standalone MCP server (feast mcp) as a container whose
entrypoint is the server.
A thin wrapper over the published feature-server image. That image installs
feast[minimal], which pulls mcp-server — FastMCP and friends — so
everything feast mcp needs is already present. This image only sets the
entrypoint, and inherits the UBI base and the arbitrary-uid permission setup.
docker buildx build -f sdk/python/feast/mcp/docker/Dockerfile \
--build-arg BASE_TAG=0.66.0 -t feast-mcp:0.66.0 --load .BASE_IMAGE and BASE_TAG select the feature-server image to build on.
BASE_IMAGE defaults to quay.io/feastdev/feature-server. BASE_TAG is
required and has no latest default, so every build uses the same base. To
build against a different repository or a published tag:
docker buildx build -f sdk/python/feast/mcp/docker/Dockerfile \
--build-arg BASE_IMAGE=quay.io/feastdev/feature-server \
--build-arg BASE_TAG=0.65.0 \
-t feast-mcp:0.66.0 --load .Add --platform linux/amd64,linux/arm64 --push for a multi-arch build.
The base tag must be a feature-server build whose minimal extra already
includes mcp-server; older tags have no feast mcp command.
CI calls make build-feast-mcp-docker. BASE_TAG defaults to VERSION:
make build-feast-mcp-docker REGISTRY=quay.io/feastdev VERSION=0.66.0 \
DOCKER_PUSH=true DOCKER_PLATFORMS=linux/amd64,linux/arm64| Trigger | Workflow | Repository | Tags |
|---|---|---|---|
Release tag v*.*.* |
publish_images.yml |
quay.io/feastdev/feast-mcp |
version, latest |
Merge to master |
master_only.yml |
quay.io/feastdev-ci/feast-mcp |
commit SHA, develop |
Both run after the image matrix, not inside it, since the feature-server image they wrap has to be pushed first.
Everything — transport, host, port, and the Feast server URLs — comes from
feast_mcp.yaml. The image's default CMD is --config /config/feast_mcp.yaml, so mount the file there. Start from
sample.feast_mcp.yaml.
Resolution order is CLI > environment > file > defaults. Environment outranks
the file, so avoid setting FEAST_MCP_* variables alongside a mounted config
— FEAST_MCP_TRANSPORT in a CR's config.env would silently override
server.transport in the yaml. The image deliberately bakes in no FEAST_MCP_*
defaults for that reason.
--host and --port have no environment equivalent at all; they come from the
yaml or the command line only.
docker run --rm -p 8000:8000 -v "$PWD/feast_mcp.yaml:/config/feast_mcp.yaml:ro" feast-mcp:0.66.0To point at a different path, or to skip the file entirely, pass your own
arguments — they replace the default CMD:
docker run --rm -p 8000:8000 feast-mcp:0.66.0 --feast-url http://host.docker.internal:6566 --transport httpFor a stdio client, set server.transport: stdio in the yaml (or pass
--transport stdio) and run with -i.
The mcp-lifecycle-operator
MCPServer CRD exposes only config.arguments — there is no command
override — which is why the entrypoint is baked into the image rather than set
in the pod spec.
apiVersion: mcp.x-k8s.io/v1alpha1
kind: MCPServer
metadata:
name: feast-mcp
spec:
source:
type: ContainerImage
containerImage:
ref: quay.io/feastdev/feast-mcp:0.66.0
config:
# Must match server.port in feast_mcp.yaml.
port: 8000
path: /mcp
storage:
# The image's default CMD reads /config/feast_mcp.yaml. Leaving
# config.arguments unset keeps that default in place.
- path: /config
source:
type: ConfigMap
configMap:
name: feast-mcp-config
# Python needs a writable temp dir under readOnlyRootFilesystem.
- path: /tmp
permissions: ReadWrite
source:
type: EmptyDir
mcp:
stateless: trueFour things that are easy to get wrong:
spec.config.portmust matchserver.portin the yaml. The operator uses its value for the Service and the container port, but only the yaml decides what the process actually binds.- Health probes should use
httpGet: /health, not/healthz. The server exposes an unauthenticatedGET /healthalongside/mcp(or/sse). AtcpSocketprobe also works if you prefer not to hit the HTTP path. - The MCP server runs in its own Deployment, not alongside the feature
server, so
features.urlhas to be the Feast Service's cluster DNS name.localhostonly works when the Feast operator runs MCP as a container in the shared Feast pod. mcp.stateless: truedrops the Service's ClientIP affinity. Fine forauth.mode: passthrough, butauth.mode: oidckeeps its authorize/callback state per-node, so a callback can land on a replica that never saw the authorize. Keep the affinity and a single replica for OIDC.
If you would rather keep the config path out of the image, set it explicitly —
config.arguments replaces the default CMD:
arguments: [--config, /etc/mcp-config/feast_mcp.yaml]
storage:
- path: /etc/mcp-config
source:
type: ConfigMap
configMap:
name: feast-mcp-configA --config path that does not exist raises FileNotFoundError, so mount the
ConfigMap in the same CR that names the flag.