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}:8080and127.0.0.1:${WEB_PORT}:8081 - Postgres and Redis are not host-exposed
- Default host ports are
8080and8081 - Some environments use
8092and8093by setting env vars
Security and runtime constraints
- App container runs as non-root
65532:65532 - Root filesystem is read-only with
/tmpmounted as tmpfs - Linux capabilities dropped (
ALL) no-new-privileges:trueenabledpostgresandredishealth 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_PASSWORDREDIS_PASSWORDMCP_PORT/WEB_PORT(use custom ports like8092/8093if 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/ssecreates a session and sends anendpointevent- The event data includes the required
session_idin message URL POST /mcp/v1/message?session_id=...usually returns202 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: messagewith 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 ReadonlyRootfsistrue- Capabilities show
ALLdropped - Security options include
no-new-privileges:true /tmptmpfs mount is present
9) Common Failure Modes and Fixes
Symptom: Missing session_id parameter
- Cause: posting to
/mcp/v1/messagewithout query string - Fix: always post to URL emitted in SSE
endpointevent
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-postgrespodman logs guardrail-redis- confirm
.envcredentials match compose env
Symptom: cannot access from outside deployment host
- Cause: ports are bound to
127.0.0.1only - 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 downpodman-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