claw-code/.guardrails/docs/RELEASE_TESTERS.md

11 KiB

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:

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/.

podman --version
podman-compose --version

If you are Docker-only (no Podman), use:

docker --version
docker compose version

Verify no local port conflict on your target host ports:

ss -ltnp | grep -E ":(8080|8081|8092|8093)"

If testing from your laptop against the deployment host, create an SSH tunnel first:

ssh -L 8092:127.0.0.1:8092 -L 8093:127.0.0.1:8093 <user>@<host>

Then use http://localhost:8092 and http://localhost:8093 locally.


3) Environment Setup

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:

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

cd mcp-server
make docker-up

Option B: Explicit compose command

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:

cd mcp-server
docker compose -f deploy/podman-compose.yml up -d --build

Tester-validated Docker variant (recommended when using Docker Desktop/Engine):

cd mcp-server
docker compose -f deploy/docker-compose.example.yml up -d --build

Check status:

podman-compose -f deploy/podman-compose.yml ps
podman ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

Docker equivalent:

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:

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):

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:

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):

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:

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

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):

curl -i "$WEB_BASE/api/documents"
curl -i "$WEB_BASE/api/rules"

Protected (no API key should fail):

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 <MCP_API_KEY>.


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:

curl -N "$MCP_BASE/mcp/v1/sse"

You should see something like:

event: endpoint
data: http://localhost:<MCP_PORT>/mcp/v1/message?session_id=sess_abc123...

: ping

Copy the full message?session_id=... URL.

Terminal B - send initialize request to that URL:

curl -i -X POST "$MCP_BASE/mcp/v1/message?session_id=<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

curl -i -X POST "$MCP_BASE/mcp/v1/message?session_id=<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

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:

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:

**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: <deployment-host>
- OS:
- Podman version:
- podman-compose version:
- Tested commit/tag:

11) Quick Command Block

# 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:

# 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


Last Updated: 2026-02-08