Deploying mcp serve

braised mcp serve is a long-running daemon. This page covers running it in production behind a reverse proxy, keeping it current after builds, and monitoring its operation.

Prerequisites

A braised build must complete before starting the server. The server reads artifacts/ at startup and will not have tools to register if the directory is empty or stale.

Check artifacts/ and .braised/last_build.json before starting:

# Confirm the last build completed cleanly
cat .braised/last_build.json
# {"completed_at":"...","steps":{"artifacts":"ok","pages":"ok",...}}

# Confirm artifacts are present
ls artifacts/
# frontmatter-index.json  nav-structure.json  sources-index.json

Local validation

Before deploying, verify the server works and all expected tools are registered.

braised mcp serve &

# Initialize and capture the session ID
SESSION=$(curl -si -X POST http://127.0.0.1:8081/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {},
      "clientInfo": {"name": "test", "version": "1.0"}
    }
  }' | grep -i "Mcp-Session-Id:" | awk '{print $2}' | tr -d '\r')

# List registered tools
curl -s -X POST http://127.0.0.1:8081/mcp \
  -H "Content-Type: application/json" \
  -H "Mcp-Session-Id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  | python3 -c "import json,sys; [print(t['name']) for t in json.load(sys.stdin)['result']['tools']]"

# Fetch a known page
curl -s -X POST http://127.0.0.1:8081/mcp \
  -H "Content-Type: application/json" \
  -H "Mcp-Session-Id: $SESSION" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {"name": "get_page", "arguments": {"path": "/"}}
  }' | python3 -c "
import json,sys
r=json.load(sys.stdin)['result']
print('isError:', r.get('isError', False))
print('length:', len(r['content'][0]['text']), 'chars')
"

The tool list in the startup log must match what you see in tools/list. If get_theme_variables is missing, artifacts/theme-variables.json was not generated — that is expected until the CSS extractor is in use.

Nginx reverse proxy

braised mcp serve binds to 127.0.0.1 only. Expose it externally via nginx. Streamable HTTP (the MCP transport braised uses) works with standard proxy configuration — no proxy_buffering off or long-poll tuning required.

server {
    listen 443 ssl;
    server_name mcp.docs.example.com;

    ssl_certificate     /etc/letsencrypt/live/mcp.docs.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mcp.docs.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # Optional: rate limiting (adjust zone and rate to your traffic)
        limit_req zone=mcp_zone burst=20 nodelay;
    }
}

# Define the rate limit zone in your http block
http {
    limit_req_zone $binary_remote_addr zone=mcp_zone:10m rate=30r/m;
}

braised mcp serve provides no authentication. Use nginx auth_request to gate access if the server is public-facing.

Systemd service

# /etc/systemd/system/braised-mcp.service
[Unit]
Description=Braised MCP Server
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/my-docs
ExecStart=/usr/local/bin/braised mcp serve
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
systemctl enable braised-mcp
systemctl start braised-mcp

Keeping artifacts current

Artifact files are loaded at startup and held in memory. The server does not watch for changes. After a full braised build, restart the server to pick up updated artifacts.

Typical CI/CD pattern:

braised build                        # writes artifacts/ and dist/
systemctl restart braised-mcp        # reloads updated artifacts
rsync -av dist/ user@host:/var/www/  # deploy HTML separately if needed

get_page is the exception — it reads companion Markdown files from disk at request time. Incremental rebuilds (braised serve in dev) keep get_page fresh without a server restart. All other tools (get_nav_structure, get_page_index, get_theme_variables) remain frozen at the last full build until the server restarts.

Monitoring

The server writes structured JSON logs to stdout. Pipe to your log aggregator or use jq locally:

# Follow live with pretty-printing
braised mcp serve 2>&1 | jq .

# Count tool calls by tool name
journalctl -u braised-mcp --no-pager | \
  jq -r 'select(.msg=="tool_call") | .tool' | sort | uniq -c | sort -rn

# Find errors
journalctl -u braised-mcp --no-pager | \
  jq 'select(.level=="ERROR" or (.msg=="tool_call" and .status=="error"))'

Key log fields per tool call: tool, duration_ms, status (ok or error), error (on error only). Tool errors are returned to the MCP client as tool-level errors — they do not crash or restart the server.