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>(fromcloudflared 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:404Service commands:
sudo systemctl enable --now cloudflared
sudo systemctl restart cloudflared
sudo systemctl status cloudflared3) 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=productionImportant: 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: failReplace 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