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 org platform mode, signed-in users see the mcp-servers-org authenticated catalog and their authorized team namespaces. --platform-mode public adds 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/v1/runtime/servers, except public mode can read mcp-servers-public
User in org mode MCPs from mcp-servers-org and authorized team namespaces
User in public mode MCPs from public, org, and authorized team namespaces
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/oauth-example-go-2025-11-25-gateway.yaml <<'EOF'
version: v1
servers:
  - name: oauth-example-go-2025-11-25-gateway
    description: Workspace assistant MCP server for task cards, release notes, and text cleanup.
    namespace: mcp-servers
    route: /oauth-example-go-2025-11-25-gateway/mcp
    publicPathPrefix: oauth-example-go-2025-11-25-gateway
    port: 8088
    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:
      issuerURL: http://localhost:18080/oauth
      audience: http://localhost:18080/demo/mcp
    policy:
      mode: allow-list
      defaultDecision: deny
      policyVersion: v1
    session:
      required: true
    gateway:
      enabled: true
EOF

Build, push, and deploy:

./bin/mcp-runtime server build image oauth-example-go-2025-11-25-gateway \
  --metadata-file /tmp/oauth-example-go-2025-11-25-gateway.yaml \
  --dockerfile examples/oauth-example-go-2025-11-25/Dockerfile \
  --context examples/oauth-example-go-2025-11-25 \
  --tag dev

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

# `server build image` updates the metadata with the resolved registry image
# and tags that exact image locally. Push that image ref, not a guessed short
# name, so the push command and deploy metadata stay in sync.
IMAGE_REF="$(python3 - <<'PY'
image = tag = ""
with open('/tmp/oauth-example-go-2025-11-25-gateway.yaml') as f:
    for line in f:
        stripped = line.strip()
        if stripped.startswith("image: "):
            image = stripped.split(":", 1)[1].strip()
        elif stripped.startswith("imageTag: "):
            tag = stripped.split(":", 1)[1].strip()
if not image or not tag:
    raise SystemExit("metadata missing image/imageTag; rerun server build image")
print(f"{image}:{tag}")
PY
)"
./bin/mcp-runtime server push \
  --image "$IMAGE_REF"

./bin/mcp-runtime server deploy oauth-example-go-2025-11-25-gateway \
  --scope tenant \
  --metadata-file /tmp/oauth-example-go-2025-11-25-gateway.yaml
SERVER_NAMESPACE="$(
  kubectl get deploy -A -l app=oauth-example-go-2025-11-25-gateway \
    -o jsonpath='{.items[0].metadata.namespace}'
)"
kubectl rollout status deploy/oauth-example-go-2025-11-25-gateway -n "$SERVER_NAMESPACE" --timeout=180s

Inspect runtime outputs

SERVER=oauth-example-go-2025-11-25-gateway
NAMESPACE="$(
  kubectl get deploy -A -l app="$SERVER" \
    -o jsonpath='{.items[0].metadata.namespace}'
)"

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

The example below is a contributor-only runtime policy test. It uses synthetic identity values and the explicit --use-kube path to test the gateway's CRD policy behavior in a disposable cluster. It does not test managed-agent directory checks or platform-issued sessions. For a platform-backed access flow, use the Quickstart or the staging E2E guide.

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

Use init to scaffold manifests. In this test, apply both resources with --use-kube; this intentionally bypasses platform API identity validation. Platform API session apply requires an admin role. For normal agent use, prefer the adapter path described in the user docs.

./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 oauth-example-go-2025-11-25-gateway \
  --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 oauth-example-go-2025-11-25-gateway \
  --human-id local-user \
  --agent-id local-agent \
  --trust high \
  --output /tmp/session.yaml

./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 an OAuth token containing Tenant B's team, human, agent, and session claims. Reusing the same session with Tenant A's team claim 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/v1/runtime/servers |
  jq '{count: (.servers|length), names: [.servers[] | (.namespace + "/" + .name)]}'