# MCP Server Tester Guide (Container Deployment) > This guide is for external testers validating the Guardrail MCP server stack on a deployment host. **Release Target:** v1.9.6 **Branch:** `mcpserver` **Status:** Testing in progress --- ## 1) Container Architecture (Current) The deployment is a **3-container stack** with strict network separation: ```text Deployment host | |-- guardrail-mcp-server (app) | |-- container port 8080: MCP SSE + JSON-RPC message endpoint | |-- container port 8081: Web UI + REST API + health + metrics | |-- attached networks: frontend, backend | |-- guardrail-postgres (state) | |-- container port 5432 (backend network only) | |-- attached networks: backend | |-- persistent volume: pg_data | |-- guardrail-redis (cache/rate limiting) | |-- container port 6379 (backend network only) | |-- attached networks: backend | |-- persistent volume: redis_data ``` ### Host exposure model - MCP and Web ports are bound to **loopback only** via compose: `127.0.0.1:${MCP_PORT}:8080` and `127.0.0.1:${WEB_PORT}:8081` - Postgres and Redis are **not** host-exposed - Default host ports are `8080` and `8081` - Some environments use `8092` and `8093` by setting env vars ### Security and runtime constraints - App container runs as non-root `65532:65532` - Root filesystem is read-only with `/tmp` mounted as tmpfs - Linux capabilities dropped (`ALL`) - `no-new-privileges:true` enabled - `postgres` and `redis` health checks gate app startup --- ## 2) Pre-Flight Checks (Before Testing) Run these from `mcp-server/`. ```bash podman --version podman-compose --version ``` If you are Docker-only (no Podman), use: ```bash docker --version docker compose version ``` Verify no local port conflict on your target host ports: ```bash ss -ltnp | grep -E ":(8080|8081|8092|8093)" ``` If testing from your laptop against the deployment host, create an SSH tunnel first: ```bash ssh -L 8092:127.0.0.1:8092 -L 8093:127.0.0.1:8093 @ ``` Then use `http://localhost:8092` and `http://localhost:8093` locally. --- ## 3) Environment Setup ```bash cd mcp-server cp .env.example .env ``` Set at minimum: - `MCP_API_KEY` (32+ chars, mixed case + digits) - `IDE_API_KEY` (32+ chars, mixed case + digits) - `JWT_SECRET` (32+ chars minimum; 64+ recommended) - `DB_PASSWORD` - `REDIS_PASSWORD` - `MCP_PORT` / `WEB_PORT` (use custom ports like `8092` / `8093` if required) Example secure generation: ```bash openssl rand -hex 32 # MCP_API_KEY openssl rand -hex 32 # IDE_API_KEY openssl rand -hex 64 # JWT_SECRET openssl rand -base64 32 # DB/Redis passwords ``` --- ## 4) Start the Stack ### Option A: Build and run on target host ```bash cd mcp-server make docker-up ``` ### Option B: Explicit compose command ```bash cd mcp-server podman-compose -f deploy/podman-compose.yml up -d --build ``` ### Option C: Docker only (no Podman) The Makefile targets use Podman tools. If you are Docker-only, run compose commands directly: ```bash cd mcp-server docker compose -f deploy/podman-compose.yml up -d --build ``` Tester-validated Docker variant (recommended when using Docker Desktop/Engine): ```bash cd mcp-server docker compose -f deploy/docker-compose.example.yml up -d --build ``` Check status: ```bash podman-compose -f deploy/podman-compose.yml ps podman ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" ``` Docker equivalent: ```bash docker compose -f deploy/podman-compose.yml ps docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" ``` If started with the Docker example file, use: ```bash docker compose -f deploy/docker-compose.example.yml ps ``` Expected containers: - `guardrail-postgres` (healthy) - `guardrail-redis` (healthy) - `guardrail-mcp-server` (running) --- ## 5) Run Database Migrations (Required) If migrations are skipped, many API operations fail even when containers look healthy. Use this compose-safe method (works even when Postgres is not host-exposed): ```bash cd mcp-server set -a source .env set +a for f in internal/database/migrations/*_up.sql; do echo "Applying $f" podman exec -i guardrail-postgres psql -U "$DB_USER" -d "$DB_NAME" < "$f" done ``` Docker equivalent: ```bash cd mcp-server set -a source .env set +a for f in internal/database/migrations/*_up.sql; do echo "Applying $f" docker exec -i guardrail-postgres psql -U "$DB_USER" -d "$DB_NAME" < "$f" done ``` Optional (only if DB is directly reachable from host): ```bash make migrate-up DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@127.0.0.1:${DB_PORT}/${DB_NAME}?sslmode=disable" ``` --- ## 6) Smoke Test Checklist Define base URLs from your `.env`: ```bash set -a source .env set +a export MCP_BASE="http://localhost:${MCP_PORT}" export WEB_BASE="http://localhost:${WEB_PORT}" ``` ### 6.1 Web health and metrics ```bash curl -s "$WEB_BASE/health/live" curl -s "$WEB_BASE/health/ready" curl -s "$WEB_BASE/version" curl -s "$WEB_BASE/metrics" | head -n 5 ``` Expected: - `/health/live` -> `200`, `status: alive` - `/health/ready` -> `200`, `status: ready` - `/version` -> service/version JSON - `/metrics` -> Prometheus text output ### 6.2 Public vs protected Web API behavior Public (no API key expected): ```bash curl -i "$WEB_BASE/api/documents" curl -i "$WEB_BASE/api/rules" ``` Protected (no API key should fail): ```bash curl -i -X POST "$WEB_BASE/api/rules" \ -H "Content-Type: application/json" \ -d '{"rule_id":"TEST-001","name":"x","pattern":"x","message":"x","severity":"warning","category":"test","enabled":true}' ``` Expected: `401 Unauthorized` without `Authorization: Bearer `. --- ## 7) MCP Protocol Test Flow (Important) This is where most tester confusion happens. ### Key behavior - `GET /mcp/v1/sse` creates a session and sends an `endpoint` event - The event data includes the required `session_id` in message URL - `POST /mcp/v1/message?session_id=...` usually returns `202 Accepted` (no JSON body) - The actual JSON-RPC response is delivered on the SSE stream as `event: message` ### Step-by-step (2 terminals) Terminal A - keep SSE open: ```bash curl -N "$MCP_BASE/mcp/v1/sse" ``` You should see something like: ```text event: endpoint data: http://localhost:/mcp/v1/message?session_id=sess_abc123... : ping ``` Copy the full `message?session_id=...` URL. Terminal B - send initialize request to that URL: ```bash curl -i -X POST "$MCP_BASE/mcp/v1/message?session_id=" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "test-client", "version": "1.0" } } }' ``` Expected: - HTTP status from POST: `202 Accepted` - Terminal A receives `event: message` with JSON-RPC result payload ### Tool call example ```bash curl -i -X POST "$MCP_BASE/mcp/v1/message?session_id=" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "guardrail_validate_git_operation", "arguments": { "command": "push", "is_force": true } } }' ``` Expected `event: message` payload includes a violation for force push. --- ## 8) Container Security Verification ```bash podman inspect guardrail-mcp-server --format '{{.Config.User}}' podman inspect guardrail-mcp-server --format '{{.HostConfig.ReadonlyRootfs}}' podman inspect guardrail-mcp-server --format '{{json .HostConfig.CapDrop}}' podman inspect guardrail-mcp-server --format '{{json .HostConfig.SecurityOpt}}' podman inspect guardrail-mcp-server --format '{{json .HostConfig.Tmpfs}}' ``` Docker equivalent: ```bash docker inspect guardrail-mcp-server --format '{{.Config.User}}' docker inspect guardrail-mcp-server --format '{{.HostConfig.ReadonlyRootfs}}' docker inspect guardrail-mcp-server --format '{{json .HostConfig.CapDrop}}' docker inspect guardrail-mcp-server --format '{{json .HostConfig.SecurityOpt}}' docker inspect guardrail-mcp-server --format '{{json .HostConfig.Tmpfs}}' ``` Expected: - User is `65532:65532` - `ReadonlyRootfs` is `true` - Capabilities show `ALL` dropped - Security options include `no-new-privileges:true` - `/tmp` tmpfs mount is present --- ## 9) Common Failure Modes and Fixes ### Symptom: `Missing session_id parameter` - Cause: posting to `/mcp/v1/message` without query string - Fix: always post to URL emitted in SSE `endpoint` event ### Symptom: POST returns `202` but no JSON body - Cause: expected behavior in session/SSE mode - Fix: read JSON-RPC response from Terminal A SSE stream (`event: message`) ### Symptom: `/health/ready` returns `503` - Cause: DB or Redis unhealthy/unreachable - Fix: - `podman logs guardrail-postgres` - `podman logs guardrail-redis` - confirm `.env` credentials match compose env ### Symptom: cannot access from outside deployment host - Cause: ports are bound to `127.0.0.1` only - Fix: use SSH tunnel or put Nginx/Traefik in front ### Symptom: Web UI loads blank/missing assets - Cause: stale container image before static asset packaging updates - Fix: - `podman-compose -f deploy/podman-compose.yml down` - `podman-compose -f deploy/podman-compose.yml up -d --build` --- ## 10) Bug Report Template Use this exact format in issues: ```markdown **Test Area:** [Architecture | MCP Protocol | Web API | Security | Resilience] **Severity:** [Critical | High | Medium | Low] **Expected:** [What should happen] **Actual:** [What happened] **Repro Steps:** 1. ... 2. ... 3. ... **Artifacts:** - `podman-compose ps` output - `podman logs guardrail-mcp-server --tail 200` - Full curl command used (redact secrets) - Response status/body or SSE event snippet **Environment:** - Host: - OS: - Podman version: - podman-compose version: - Tested commit/tag: ``` --- ## 11) Quick Command Block ```bash # Start stack cd mcp-server && make docker-up # Check service state podman-compose -f deploy/podman-compose.yml ps # Follow app logs podman logs -f guardrail-mcp-server # Load port values set -a && source .env && set +a # Health check curl -s "http://localhost:${WEB_PORT}/health/ready" # Open SSE curl -N "http://localhost:${MCP_PORT}/mcp/v1/sse" ``` Docker-only quick block: ```bash # Start stack cd mcp-server docker compose -f deploy/podman-compose.yml up -d --build # Or use tester-validated Docker compose file # docker compose -f deploy/docker-compose.example.yml up -d --build # Check service state docker compose -f deploy/podman-compose.yml ps # Follow app logs docker logs -f guardrail-mcp-server # Load port values set -a && source .env && set +a # Health check curl -s "http://localhost:${WEB_PORT}/health/ready" # Open SSE curl -N "http://localhost:${MCP_PORT}/mcp/v1/sse" ``` --- ## Feedback Channel - GitHub Issues: https://github.com/TheArchitectit/agent-guardrails-template/issues --- **Last Updated:** 2026-02-08