489 lines
11 KiB
Markdown
489 lines
11 KiB
Markdown
# 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 <user>@<host>
|
|
```
|
|
|
|
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 <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:
|
|
|
|
```bash
|
|
curl -N "$MCP_BASE/mcp/v1/sse"
|
|
```
|
|
|
|
You should see something like:
|
|
|
|
```text
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```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: <deployment-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
|