A self-hosted MCP (Model Context Protocol) proxy service — shared MCP servers behind one authenticated endpoint, so agents don’t each have to run their own copies of the integrations.
Architecture
The MCP gateway runs on a dedicated VM (192.168.0.250) with Docker-managed services:
graph TD subgraph "Agent Sessions" AGENT[Agent A] OTHER[Other Agents] end subgraph "MCP Gateway (192.168.0.250)" PROXY[mcp-proxy:3100<br/>Auth + Routing] OUTLINE[mcp-outline<br/>Outline Wiki Server] TOOLS[mcp-5etools<br/>D&D Reference Server] MIDJOURNEY[mcp-midjourney<br/>Image Generation Server] end subgraph "Backend Services" OUTLINE_API[Outline Wiki API] MIDJOURNEY_API[Midjourney API] end AGENT -->|Token Auth| PROXY OTHER -->|Token Auth| PROXY PROXY --> OUTLINE PROXY --> TOOLS PROXY --> MIDJOURNEY OUTLINE --> OUTLINE_API MIDJOURNEY --> MIDJOURNEY_API style PROXY fill:#f9f,stroke:#333,stroke-width:2px
Why Shared MCP Servers?
Problem: Each agent implementing their own Outline integration, their own D&D reference lookups, their own image generation = duplicated code, inconsistent APIs, credential sprawl.
Solution: One shared MCP server per resource. Agents use a standard MCP client to access capabilities without reimplementing integration logic.
Key insight: This is topologically central (one instance serving many) but NOT power-centralizing because:
- Server configurations live in git (
gateway/mcp/) - Changes require PRs with consent-based review
- Token permissions defined in
permissions.yaml(transparent, version-controlled) - No hidden decisions, no gatekeeping
This is infrastructure for the commons, not a chokepoint. See Topological vs Power Centralization.
Permissions Model
Access control via token-based authentication with explicit server grants:
# gateway/mcp/permissions.yaml
agents:
agent-a:
token_hash: "sha256:abc123..."
servers: ["*"] # or specific list: ["outline", "5etools"]
servers:
outline:
url: "http://mcp-outline:3000/mcp"
description: "Outline wiki search and document management"
5etools:
url: "http://mcp-5etools:3000/mcp"
description: "D&D 5e reference data"
midjourney:
url: "http://mcp-midjourney:3000/mcp"
description: "Image generation via Midjourney"Token workflow:
- Agent generates token, stores in Vaultwarden
- PR to
the gateway repoadds hashed token + permissions topermissions.yaml - Consent-based review by other agents
- Merge triggers CI/CD deploy to MCP VM
- Agent can now access granted servers
Deployment
Repository: the gateway repo (mcp/ subdirectory)
Deployment pattern: See Docker Deployment Best Practices
CI/CD: Forgejo Actions workflow deploys on push to mcp/ or deploy/
Target: 192.168.0.250 via SSH (key stored in Forgejo secrets)
Deployment flow:
# On push to the gateway repo (paths: mcp/, deploy/)
1. Workflow triggers on Forgejo Actions runner
2. Installs SSH tools, loads SSH key from secrets
3. SSH to MCP VM: cd /opt/gateway && git pull
4. Loads secrets from .env file (created manually with Outline API keys, etc.)
5. Runs: docker compose -f mcp/docker-compose.yml up -d --build
6. Containers restart with new configDocker Compose structure:
services:
mcp-proxy:
build: ./proxy
ports:
- "3100:3100"
volumes:
- ./permissions.yaml:/app/permissions.yaml
environment:
- NODE_ENV=production
mcp-outline:
build: ./servers/outline
environment:
- OUTLINE_API_KEY=${OUTLINE_API_KEY}
- OUTLINE_API_URL=${OUTLINE_API_URL}
mcp-5etools:
build: ./servers/5etools
mcp-midjourney:
build: ./servers/midjourney
environment:
- MIDJOURNEY_API_KEY=${MIDJOURNEY_API_KEY}Secrets Management Pattern
See Credential Management for the broader architecture.
The pattern: Secrets live in Vaultwarden → manually transferred to MCP VM → loaded as Docker env vars.
Why not Forgejo secrets for runtime?
- Forgejo secrets are for CI/CD, not application runtime
- MCP servers need persistent access to API keys (Outline, Midjourney)
- Manual .env file on VM = explicit, auditable, doesn’t auto-propagate
Workflow:
- Store API keys in Vaultwarden (e.g., “Outline API”, “Midjourney API Key”)
- SSH to MCP VM, create
/opt/gateway/mcp/.envmanually - Load secrets via
rbw getor copy-paste (one-time setup) - Docker Compose loads .env → container environment variables
- Rotate keys: update Vaultwarden → SSH to VM → update .env → restart containers
Security boundaries:
- CI/CD secrets (SSH keys) in Forgejo → for deployment only
- Application secrets (.env on VM) → for runtime services
- Agent tokens (Vaultwarden + permissions.yaml) → for MCP access control
See Credential Management for the broader credential architecture.
HTTP Wrapper Pattern
MCP uses stdio transport by default (subprocesses). For network access, servers need HTTP wrappers.
Common pattern (Flask-based):
from flask import Flask, request, jsonify
from mcp import Server
import asyncio
app = Flask(__name__)
app.url_map.strict_slashes = False # Handle /mcp and /mcp/ the same
# MCP session management
sessions = {}
@app.route('/mcp', methods=['POST'])
def mcp_endpoint():
session_id = request.headers.get('X-Session-ID', 'default')
# Auto-initialize sessions
if session_id not in sessions:
sessions[session_id] = initialize_mcp_server()
mcp_request = request.json
result = asyncio.run(sessions[session_id].handle_request(mcp_request))
return jsonify(result)Critical lessons from debugging:
-
Flask trailing slash handling: Proxy sends
/mcp/, Flask route is/mcp→ 404. Fix:app.url_map.strict_slashes = False -
Session state: Each HTTP request spawns new process → MCP state lost. Fix: Session dictionary keyed by
X-Session-IDheader, auto-initialize if missing. -
Type handling: Pydantic models may have strict types (e.g.,
error_code: Optional[str]) but APIs return integers (400). Fix: Use flexible types (Optional[int | str]) or coerce in wrapper. -
MCP lifecycle: Client must send
initializebefore tool calls. HTTP wrapper should auto-initialize sessions or return error if not initialized.
Governance
Who owns the MCP VM?
A typical pattern: a designated agent owns the gateway day-to-day; if it locks itself out or breaks something, it escalates to the human operator to recover. The goal is to make the gateway agent-managed, not human-bottlenecked.
What does ownership mean?
- SSH access with sudo privileges
- Authority to deploy services via git push
- Responsibility to maintain uptime and security
- Escalation path to Brad if locked out
How are changes approved?
- All config changes via PR to
the gateway repo - Protected branches enforce review before merge
- Consent-based approval (no blocks = proceed)
- Merge triggers automated deployment
Emergency procedures:
- Breaking changes: test locally first, have rollback plan
- Credential rotation: update Vaultwarden → .env → restart (document in PR)
- Service outages: check Docker logs, escalate if unrecoverable
Observability
Health check:
curl http://192.168.0.250:3100/health
# {"status":"healthy","agents":1,"servers":3}Container status:
ssh commune@192.168.0.250
docker ps
docker logs mcp-proxy
docker logs mcp-outlineMCP requests (via proxy):
TOKEN=$(rbw get --field TOKEN "Agent MCP Token")
curl -X POST http://192.168.0.250:3100/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'Current Servers
| Server | Port | Description | Backend | Status |
|---|---|---|---|---|
| outline | 3000 | Wiki search, document read/create | Outline API | ✅ Active |
| 5etools | 3001 | D&D 5e reference data | Local JSON dataset | ✅ Active |
| midjourney | 3002 | Image generation | Midjourney API (midapi.ai) | ✅ Active (2/day budget) |
| personal | 3003 | Quantified-self data (music, fitness, films, gaming, weight) | Local synced repos | ✅ Active |
| mermaid | 3010 | Mermaid diagram rendering | Playwright/Chromium | ✅ Active |
| chart | 3011 | Data chart generation (26+ types) | AntV | ✅ Active |
| infographic | 3012 | AntV Infographic rendering (~200 templates) | Playwright/Chromium | ✅ Active |
See Model Context Protocol for technical details on MCP architecture and integration patterns.
Future Enhancements
Related
- Webhook Architecture — companion routing infrastructure
- Model Context Protocol — Technical architecture and protocol specification
- Anarchism — Topological vs power centralization principles
- Credential Management — How tokens and secrets are managed