Skip to content

Service Iteration

After setup --test-mode succeeds, do not rerun full setup for every code change. Run the targeted tests, rebuild the image for the changed component, and roll only that Deployment.

Test First

Use focused tests while iterating:

go test ./internal/operator/... ./internal/cli/... -count=1
go test ./internal/agentadapter -count=1
(cd services/platform-api && go test ./... -count=1)
(cd services/runtime-api && go test ./... -count=1)
(cd services/analytics-api && go test ./... -count=1)
(cd services/ui && go test ./... -count=1)
(cd services/ui/frontend && npm run build && npm run test)

Run wider checks before handing off a broad change:

gofmt -s -l .
go vet ./...
go test ./... -count=1
git diff --check

API and UI

API and UI changes often need coordinated rollout when browser flows depend on new /api/v1/* behavior. Traefik routes API traffic directly; the UI serves static assets and auth session cookies only.

The UI service remains a Go backend for /config.js, /auth/*, API proxying, security headers, and static asset embedding. React, Vite, and TypeScript source lives under services/ui/frontend; Vite writes the browser bundle into services/ui/static for the Go embed step. During the migration, the current dashboard is shipped from frontend/public/legacy and mounted by the React shell, so migrate tabs into React one at a time.

Build and roll the UI:

SERVICE=ui
IMAGE_REPO=mcp-sentinel-ui
DOCKERFILE=services/ui/Dockerfile
BUILD_CONTEXT=.
DEPLOYMENT=mcp-sentinel-ui
CONTAINER=ui
TAG="${SERVICE}-dev-$(date +%s)"
LOCAL_IMAGE="${IMAGE_REPO}:${TAG}"
REGISTRY=registry.registry.svc.cluster.local:5000

docker build -t "$LOCAL_IMAGE" -f "$DOCKERFILE" "$BUILD_CONTEXT"

./bin/mcp-runtime auth login --api-url http://localhost:18080

./bin/mcp-runtime registry push \
  --image "$LOCAL_IMAGE" \
  --name "$IMAGE_REPO"

kubectl -n mcp-sentinel set image \
  "deployment/$DEPLOYMENT" \
  "$CONTAINER=$REGISTRY/$IMAGE_REPO:$TAG"

kubectl -n mcp-sentinel rollout status "deployment/$DEPLOYMENT" --timeout=90s

Use the same shape for each split API service:

Service IMAGE_REPO DOCKERFILE DEPLOYMENT CONTAINER Port
platform-api mcp-platform-api services/platform-api/Dockerfile mcp-platform-api platform-api 8080
runtime-api mcp-runtime-api services/runtime-api/Dockerfile mcp-runtime-api runtime-api 8084
analytics-api mcp-analytics-api services/analytics-api/Dockerfile mcp-analytics-api analytics-api 8085

Set BUILD_CONTEXT=. and pick a unique TAG per build. Example for platform-api:

SERVICE=platform-api
IMAGE_REPO=mcp-platform-api
DOCKERFILE=services/platform-api/Dockerfile
DEPLOYMENT=mcp-platform-api
CONTAINER=platform-api

Use a new tag for every build. Reusing latest with IfNotPresent can leave old images cached on the node.

Ingest and Processor

For analytics pipeline changes:

Service Image repo Dockerfile Build context Deployment Container
Ingest mcp-sentinel-ingest services/ingest/Dockerfile . mcp-sentinel-ingest ingest
Processor mcp-sentinel-processor services/processor/Dockerfile . mcp-sentinel-processor processor

After rolling either service, generate one MCP request and check both logs (admin kubectl):

./bin/mcp-runtime sentinel logs ingest --since 10m
./bin/mcp-runtime sentinel logs processor --since 10m
./bin/mcp-runtime sentinel events

Operator

Operator changes affect how MCPServer, MCPAccessGrant, and MCPAgentSession objects reconcile. Run the operator tests first:

go test ./internal/operator/... -count=1

For a local Kind-only debug build, build an image, load it into the Kind node, and roll the operator:

TAG="operator-dev-$(date +%s)"
IMAGE="registry.registry.svc.cluster.local:5000/mcp-runtime-operator:${TAG}"

DOCKER_BUILDKIT=0 docker build --platform=linux/$(go env GOARCH) \
  -t "$IMAGE" \
  -f Dockerfile.operator .

kind load docker-image "$IMAGE" --name mcp-runtime

kubectl patch deployment/mcp-runtime-operator-controller-manager \
  -n mcp-runtime \
  --type=json \
  -p='[{"op":"replace","path":"/spec/template/spec/containers/0/imagePullPolicy","value":"IfNotPresent"}]'

kubectl set image deployment/mcp-runtime-operator-controller-manager \
  -n mcp-runtime \
  manager="$IMAGE"

kubectl rollout status deployment/mcp-runtime-operator-controller-manager \
  -n mcp-runtime \
  --timeout=180s

Then watch reconciliation:

kubectl logs -n mcp-runtime deploy/mcp-runtime-operator-controller-manager --since=10m
kubectl get mcpservers -A
kubectl get deploy,svc,ingress -A | rg '<server-name>|mcp-runtime'

If you changed CRD fields, apply the CRD before expecting old controllers or new controllers to agree on validation:

kubectl apply -f config/crd/bases/mcpruntime.org_mcpservers.yaml

MCP Gateway Sidecar

services/mcp-gateway runs as the mcp-gateway sidecar in each MCP server pod. To test gateway changes, rebuild and push mcp-sentinel-mcp-gateway, update the operator's MCP_GATEWAY_PROXY_IMAGE, roll the operator, then restart affected MCP server pods so the sidecar is reinjected.

Check the gateway sidecar:

POD="$(kubectl get pods -n <namespace> -l app=<server-name> -o jsonpath='{.items[0].metadata.name}')"
kubectl logs -n <namespace> "$POD" -c mcp-gateway
kubectl describe pod -n <namespace> "$POD"

Registry Notes

Inside Kubernetes, image references use registry.registry.svc.cluster.local:5000. Your host usually cannot resolve that DNS name. Prefer mcp-runtime registry push, which uses an in-cluster helper after platform login, or kind load docker-image for single-node Kind debugging.

Raw docker push registry.registry.svc.cluster.local:5000/... from the host is expected to fail unless you have added host DNS and insecure registry settings.