GM Read-Only Setup Config Pack

Foundry localhost + foundry-vtt-mcp + Cloudflare Tunnel (read-only ingestion)

Use this as a copy/paste pack during GM setup.


0) Target outcome

  • Foundry MCP remains on GM localhost
  • Exposed through Cloudflare Tunnel
  • Protected by Cloudflare Access service token
  • Your Pathfinder ingestion can read only
  • No write/mutate operations enabled

1) Variables to decide first

Replace placeholders:

  • <MCP_PORT> (example: 31415)
  • <HOSTNAME> (example: mcp-foundry.example.com)
  • <TUNNEL_NAME> (example: foundry-mcp)
  • <TUNNEL_UUID> (from cloudflared tunnel create)

2) cloudflared config (GM host)

File: /etc/cloudflared/config.yml

tunnel: <TUNNEL_NAME>
credentials-file: /etc/cloudflared/<TUNNEL_UUID>.json
 
ingress:
  - hostname: <HOSTNAME>
    service: http://127.0.0.1:<MCP_PORT>
  - service: http_status:404

Service commands:

sudo systemctl enable --now cloudflared
sudo systemctl restart cloudflared
sudo systemctl status cloudflared

3) Cloudflare Access policy (service token)

Create Access app for <HOSTNAME>.

Policy (minimum):

  • Default deny
  • Allow only service token used by your ingestion worker

Your ingestion requests must include:

CF-Access-Client-Id: <CLIENT_ID>
CF-Access-Client-Secret: <CLIENT_SECRET>

4) GM-side foundry-vtt-mcp run config (read-only intent)

If launching from shell/service, use env like:

FOUNDRY_HOST=localhost
FOUNDRY_PORT=<MCP_PORT>
NODE_ENV=production

Important: Keep bind host as localhost/private, never open raw MCP directly to internet.


5) Foundry module settings checklist (must-do)

Inside Foundry module settings for MCP bridge:

  • Enable MCP Bridge: ON
  • Allow Write Operations: OFF
  • Connection Type: Auto (or explicit based on your network)
  • WebSocket host: localhost (for local mode)
  • Show connection messages: optional

If there is any setting that grants content creation/updates, keep it OFF.


6) Pathfinder ingestion env block (your side)

FOUNDRY_MCP_URL="https://<HOSTNAME>"
CF_ACCESS_CLIENT_ID="<CLIENT_ID>"
CF_ACCESS_CLIENT_SECRET="<CLIENT_SECRET>"
FOUNDRY_MCP_TIMEOUT_MS="12000"
FOUNDRY_MCP_RETRIES="2"
FOUNDRY_MCP_FAIL_CLOSED="true"
FOUNDRY_MCP_ALLOWLIST_PATH="./config/foundry-mcp-allowlist.yaml"

7) Read-only adapter policy starter

File: config/foundry-mcp-allowlist.yaml

version: 1
mode: deny_by_default
sourceTag: foundry-live
 
auth:
  type: cloudflare_access_service_token
  required: true
  headers:
    clientId: CF-Access-Client-Id
    clientSecret: CF-Access-Client-Secret
 
transport:
  baseUrlEnv: FOUNDRY_MCP_URL
  timeoutMsEnv: FOUNDRY_MCP_TIMEOUT_MS
  retriesEnv: FOUNDRY_MCP_RETRIES
 
allow:
  - name: foundry.getWorldInfo
    class: SAFE_READ
  - name: foundry.getInitiative
    class: SAFE_READ
  - name: foundry.getPartyStatus
    class: SAFE_READ
 
deny:
  - pattern: "*.create*"
  - pattern: "*.update*"
  - pattern: "*.delete*"
  - pattern: "*.set*"
  - pattern: "*.execute*"
  - pattern: "*.eval*"
  - pattern: "*.admin*"
 
failurePolicy:
  failClosedEnv: FOUNDRY_MCP_FAIL_CLOSED
  onPolicyMismatch: deny
  onMissingAuth: deny
  onTimeout: fail

Replace tool names with actual discovered MCP tool names during recon.


8) Validation commands

From your side (without headers) should fail:

curl -i https://<HOSTNAME>

With service token should pass:

curl -i https://<HOSTNAME> \
  -H "CF-Access-Client-Id: <CLIENT_ID>" \
  -H "CF-Access-Client-Secret: <CLIENT_SECRET>"

9) Go-live checklist

  • MCP bound to localhost only
  • Cloudflare tunnel healthy
  • Access policy default deny + service token allow
  • Foundry module write operations OFF
  • Adapter deny-by-default policy active
  • Test calls return read-only data only