Skip to content

Runtime MCP Testing

Use this page to deploy MCP servers, verify catalog visibility, inspect gateway policy, and clean up stale runtime objects in a contributor cluster.

Catalog Model

mcp-servers is the legacy single-team/example namespace used by the local testing manifests below. In default tenant platform mode, signed-in users see only team namespaces they belong to. --platform-mode org uses mcp-servers-org as the shared authenticated catalog, and --platform-mode public uses mcp-servers-public as the anonymous preview catalog. Team-specific MCPs belong in namespaces such as mcp-team-tenant-a.

Expected UI/API behavior:

Principal Expected catalog
Anonymous 401 for /api/runtime/servers, except public mode can read mcp-servers-public
Normal user in tenant mode MCPs from team namespaces they belong to
Tenant user in tenant mode MCPs from their own team namespace
User in org mode MCPs from mcp-servers-org
User in public mode MCPs from mcp-servers-public
Admin Cluster-wide management visibility, with namespace/team checks on writes

Deploy the Bundled Workspace Assistant

The bundled workspace assistant sample is useful for disposable local testing. Keep it separate from long-lived shared-cluster MCPs.

Create metadata:

cat > /tmp/workspace-assistant-mcp.yaml <<'EOF'
version: v1
servers:
  - name: workspace-assistant-mcp
    description: Workspace assistant MCP server for task cards, release notes, and text cleanup.
    route: /workspace-assistant-mcp/mcp
    publicPathPrefix: workspace-assistant-mcp
    port: 8088
    namespace: mcp-servers
    envVars:
      - name: MCP_PATH
        value: /workspace-assistant-mcp/mcp
    tools:
      - name: add
        description: Add two numeric values.
        requiredTrust: low
        sideEffect: read
      - name: upper
        description: Convert text to uppercase for normalization checks.
        requiredTrust: medium
        sideEffect: read
      - name: create_task
        description: Create a deterministic task card summary.
        requiredTrust: low
        sideEffect: write
      - name: draft_release_note
        description: Draft a compact release note from a change summary and impact.
        requiredTrust: low
        sideEffect: read
    auth:
      mode: header
      humanIDHeader: X-MCP-Human-ID
      agentIDHeader: X-MCP-Agent-ID
      sessionIDHeader: X-MCP-Agent-Session
    policy:
      mode: allow-list
      defaultDecision: deny
      policyVersion: v1
    session:
      required: true
    gateway:
      enabled: true
    analytics:
      ingestURL: http://mcp-sentinel-ingest.mcp-sentinel.svc.cluster.local:8081/events
      apiKeySecretRef:
        name: workspace-assistant-mcp-analytics-creds
        key: api-key
EOF

Create the analytics Secret when you apply raw YAML with kubectl. The platform-backed mcp-runtime server deploy path creates this per-server Secret automatically.

API_KEY="$(
  kubectl get secret mcp-sentinel-secrets -n mcp-sentinel \
    -o jsonpath='{.data.INGEST_API_KEYS}' | base64 -d | cut -d, -f1
)"

kubectl create secret generic workspace-assistant-mcp-analytics-creds \
  -n mcp-servers \
  --from-literal=api-key="$API_KEY" \
  --dry-run=client -o yaml | kubectl apply -f -

Build, push, and deploy:

./bin/mcp-runtime server build image workspace-assistant-mcp \
  --metadata-file /tmp/workspace-assistant-mcp.yaml \
  --dockerfile examples/workspace-assistant-mcp/Dockerfile \
  --context examples/workspace-assistant-mcp \
  --tag dev

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

./bin/mcp-runtime registry push \
  --image workspace-assistant-mcp:dev

./bin/mcp-runtime server deploy workspace-assistant-mcp \
  --scope tenant \
  --metadata-file /tmp/workspace-assistant-mcp.yaml
kubectl rollout status deploy/workspace-assistant-mcp -n mcp-servers --timeout=180s

Inspect Runtime Outputs

SERVER=workspace-assistant-mcp
NAMESPACE=mcp-servers

kubectl get mcpserver "$SERVER" -n "$NAMESPACE" -o yaml
kubectl get deploy/"$SERVER" svc/"$SERVER" ingress/"$SERVER" -n "$NAMESPACE" -o wide
kubectl get cm -n "$NAMESPACE" "${SERVER}-gateway-policy" -o yaml

./bin/mcp-runtime auth login --api-url http://localhost:18080
./bin/mcp-runtime server policy inspect "$SERVER" --namespace "$NAMESPACE"

Admin/operator fallback when you need the raw ConfigMap JSON without platform auth:

./bin/mcp-runtime server policy inspect "$SERVER" --namespace "$NAMESPACE" --use-kube

The MCP app container is usually distroless. Use logs and kubectl describe before trying to exec a shell:

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

Grants and Sessions

Gateway policy requires both an access grant and an agent session when the server has spec.session.required=true.

Use init to scaffold manifests. Apply the grant through the platform API after auth login. Platform API session apply requires an admin role — use admin login (admin@mcpruntime.org in test mode), --use-kube, or kubectl apply for explicit curl sessions. For agent testing, prefer the adapter path documented below.

./bin/mcp-runtime auth login --api-url http://localhost:18080 \
  --email admin@mcpruntime.org --password 'admin@123'

./bin/mcp-runtime access grant init workspace-assistant-grant \
  --namespace mcp-servers \
  --server workspace-assistant-mcp \
  --human-id local-user \
  --agent-id local-agent \
  --tool add --tool upper \
  --trust high \
  --output /tmp/grant.yaml

./bin/mcp-runtime access session init local-session \
  --namespace mcp-servers \
  --server workspace-assistant-mcp \
  --human-id local-user \
  --agent-id local-agent \
  --trust high \
  --output /tmp/session.yaml

./bin/mcp-runtime access grant apply --file /tmp/grant.yaml
./bin/mcp-runtime access session apply --file /tmp/session.yaml

Admin/operator direct Kubernetes fallback:

./bin/mcp-runtime access grant apply --file /tmp/grant.yaml --use-kube
./bin/mcp-runtime access session apply --file /tmp/session.yaml --use-kube

Then verify materialization:

kubectl get mcpaccessgrant,mcpagentsession -n "$NAMESPACE" -o wide
./bin/mcp-runtime server policy inspect "$SERVER" --namespace "$NAMESPACE"

Raw ConfigMap inspection without platform auth (--use-kube):

./bin/mcp-runtime server policy inspect "$SERVER" --namespace "$NAMESPACE" --use-kube

If the CRDs exist but the policy file does not include them, check:

kubectl logs -n mcp-runtime deploy/mcp-runtime-operator-controller-manager --since=10m
kubectl get cm -n "$NAMESPACE" "${SERVER}-gateway-policy" \
  -o 'go-template={{index .data "policy.json"}}'

Tenant MCPs

For team-specific servers, use one namespace per tenant or team and set spec.teamID to the immutable platform team ID. The platform API defaults and validates this for team namespace writes; direct kubectl apply depends on the manifest and Kubernetes RBAC.

Cross-team delegation is modeled as an access resource in the server owner's namespace with an explicit foreign subject.teamID. For example, Tenant A can create a grant and session in mcp-team-tenant-a that point at tenant-a-mcp, while setting subject.teamID to Tenant B's team ID. The request must then carry Tenant B's X-MCP-Team-ID plus the matching human, agent, and session headers. Reusing the same session with Tenant A's team ID should fail with session_not_found or no_matching_grant.

Inventory command:

kubectl get mcpservers -A \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,TEAM:.spec.teamID,PATH:.spec.ingressPath,READY:.status.deploymentReady,GW:.status.gatewayReady'

Tenant visibility should be checked through the UI/API, not only kubectl, because the UI/API path enforces principal namespaces.

Remove a Stale Test MCP

Remove access resources first, then the server and single-purpose Secret:

kubectl delete mcpagentsession <session-name> -n <namespace> --ignore-not-found
kubectl delete mcpaccessgrant <grant-name> -n <namespace> --ignore-not-found
kubectl delete mcpserver <server-name> -n <namespace> --ignore-not-found
kubectl delete secret <server-name>-analytics-creds -n <namespace> --ignore-not-found

Confirm the catalog through the UI/API after cleanup:

curl -sS -b /tmp/mcp-test-user-cookie.txt \
  http://localhost:18080/api/runtime/servers |
  jq '{count: (.servers|length), names: [.servers[] | (.namespace + "/" + .name)]}'