|
| 1 | +--- |
| 2 | +title: "Warp Tunnels" |
| 3 | +description: "Connect external AI clients to private MCP servers through a self-hosted Arcade Engine, with internal connectivity, multi-network deployments, and reverse-proxy access." |
| 4 | +--- |
| 5 | + |
| 6 | +import Image from "next/image"; |
| 7 | +import { Callout, Steps, Tabs } from "nextra/components"; |
| 8 | + |
| 9 | +# Warp Tunnels |
| 10 | + |
| 11 | +Warp Tunnels connect AI clients to the MCP servers you keep off the public internet, through a self-hosted Arcade Engine that runs inside your network. This page is for platform and infrastructure teams who self-host the Arcade Engine and need to reach internal tools, such as an on-premises SAP instance, GitHub Enterprise, or a private database, without exposing them or opening inbound firewall rules. It covers how the connection works, the three deployment scenarios you can build, and the proxy and client requirements for each. |
| 12 | + |
| 13 | +<GuideOverview> |
| 14 | +<GuideOverview.Outcomes> |
| 15 | + |
| 16 | +Connect external AI clients to private MCP servers through a self-hosted Arcade Engine, while keeping your internal services off the public internet and your firewall closed to inbound traffic. |
| 17 | + |
| 18 | +</GuideOverview.Outcomes> |
| 19 | + |
| 20 | +<GuideOverview.Prerequisites> |
| 21 | + |
| 22 | +- A [self-hosted Arcade Engine](/guides/deployment-hosting/on-prem) |
| 23 | +- Access to your [engine.yaml configuration](/guides/deployment-hosting/configure-engine) |
| 24 | +- The private hostnames or IP ranges of your internal MCP servers |
| 25 | + |
| 26 | +</GuideOverview.Prerequisites> |
| 27 | + |
| 28 | +<GuideOverview.YouWillLearn> |
| 29 | + |
| 30 | +- How the Arcade Engine reaches internal MCP servers without inbound ports |
| 31 | +- How to allowlist internal addresses and register MCP servers in `engine.yaml` |
| 32 | +- How to deploy isolated Engines across multiple networks or regions |
| 33 | +- How to expose the Engine to external AI clients through a reverse proxy |
| 34 | + |
| 35 | +</GuideOverview.YouWillLearn> |
| 36 | +</GuideOverview> |
| 37 | + |
| 38 | +## How Warp Tunnels work |
| 39 | + |
| 40 | +A self-hosted Arcade Engine runs as an MCP server inside your private network, on port `9099` by default. AI clients connect to it the same way they connect to any MCP server. The Engine then calls your internal MCP servers on their behalf, handling authentication, credential management, access governance, and audit logging so your individual services don't have to. |
| 41 | + |
| 42 | +Two concepts shape every deployment: |
| 43 | + |
| 44 | +- **Gateways** are named paths on the Engine (`/mcp/{slug}`). Each gateway has its own auth mode, tool allow-list, and access rules, so an AI client connecting to `/mcp/finance` sees only finance tools. You create gateways in the [Arcade dashboard](/guides/mcp-gateways), not in `engine.yaml`. |
| 45 | +- **The Arcade Coordinator** is the shared control plane for users, organizations, API keys, RBAC, and OAuth. The Engine makes outbound-only HTTPS calls to the Coordinator; the Coordinator never dials the Engine. Multiple Engine deployments can share one Coordinator. |
| 46 | + |
| 47 | +The rest of this page covers three scenarios you can compose: reaching internal MCP servers from a single Engine, isolating Engines across multiple networks, and exposing the Engine to external AI clients. |
| 48 | + |
| 49 | +## Gateway auth modes |
| 50 | + |
| 51 | +Each gateway uses one of three auth modes. The mode determines what the AI client sends and which clients can connect. |
| 52 | + |
| 53 | +| Dashboard name | Config value | What the client sends | Works with the Anthropic Messages API? | |
| 54 | +| --- | --- | --- | --- | |
| 55 | +| Arcade Auth | `arcade_oauth` | Bearer JWT issued by Arcade OAuth | Yes | |
| 56 | +| User Source | `user_source` | Bearer JWT from your identity provider | Yes | |
| 57 | +| Arcade Headers | `arcade_header` | Bearer token plus an `Arcade-User-ID` header | No. The Anthropic connector can't send custom headers | |
| 58 | + |
| 59 | +For Claude, through the Messages API or managed agents, use **Arcade Auth** or **User Source**. See [MCP Gateways](/guides/mcp-gateways) for how to choose a mode and [User Sources](/guides/user-sources) for connecting your own identity provider. |
| 60 | + |
| 61 | +## Connect to internal MCP servers |
| 62 | + |
| 63 | +Your internal MCP servers live at private hostnames or IP addresses. Configure the `ssrf_allowlist` in `engine.yaml` to tell the Engine which internal addresses it's permitted to call, then register each MCP server URI as a worker. |
| 64 | + |
| 65 | +The Engine calls these servers directly over your private network, by their internal addresses. No inbound ports, and no tunnel, are required. |
| 66 | + |
| 67 | +<Image |
| 68 | + alt="Scenario 1: a single Arcade Engine and its internal MCP servers share one private network. AI clients connect to scoped /mcp gateways, the ssrf_allowlist gates which internal addresses the Engine reaches, and the Engine makes outbound-only HTTPS calls to the shared Coordinator." |
| 69 | + className="mt-4 h-auto w-full rounded-lg border border-border" |
| 70 | + src="/images/warp-tunnels/scenario-1.svg" |
| 71 | + width={1200} |
| 72 | + height={900} |
| 73 | + unoptimized |
| 74 | +/> |
| 75 | + |
| 76 | +Add the allowlist and workers to the `tools.directors` section of `engine.yaml`: |
| 77 | + |
| 78 | +```yaml filename="engine.yaml" |
| 79 | +tools: |
| 80 | + directors: |
| 81 | + - id: default |
| 82 | + ssrf_allowlist: |
| 83 | + - "*.corp.internal" # any subdomain |
| 84 | + - "10.10.0.0/16" # IP range |
| 85 | + workers: |
| 86 | + - id: bloomberg |
| 87 | + enabled: true |
| 88 | + http: |
| 89 | + uri: "http://bloomberg.corp.internal:8000" |
| 90 | + secret: "${env:BLOOMBERG_SECRET}" |
| 91 | + - id: sap |
| 92 | + enabled: true |
| 93 | + http: |
| 94 | + uri: "http://sap.corp.internal:8080" |
| 95 | + secret: "${env:SAP_SECRET}" |
| 96 | + - id: github-enterprise |
| 97 | + enabled: true |
| 98 | + http: |
| 99 | + uri: "http://github.corp.internal" |
| 100 | + secret: "${env:GITHUB_SECRET}" |
| 101 | +``` |
| 102 | +
|
| 103 | +For the rest of the `tools.directors` and worker options, see [Engine configuration](/guides/deployment-hosting/configure-engine#tools-configuration). |
| 104 | + |
| 105 | +### Allowlist entry types |
| 106 | + |
| 107 | +| Type | Example | Evaluated | Matches | |
| 108 | +| --- | --- | --- | --- | |
| 109 | +| Exact host | `https://host.corp:8080` | Before DNS | Scheme, host, and port exactly | |
| 110 | +| Wildcard | `*.corp.internal` | Before DNS | Any matching subdomain | |
| 111 | +| CIDR | `10.10.0.0/16` | After DNS resolution | IPs in the range | |
| 112 | + |
| 113 | +Keep these rules in mind when you write allowlist entries: |
| 114 | + |
| 115 | +- URIs must use `http://` or `https://`. The Engine rejects other schemes at startup. |
| 116 | +- A wildcard such as `*.corp.internal` does not match the bare apex `corp.internal`. Add the apex separately if the Engine needs to reach it. |
| 117 | +- CIDR entries match against the resolved IP. For split-horizon DNS, where a hostname resolves to different IPs inside and outside the network, use exact-host or wildcard entries instead. |
| 118 | +- Malformed entries cause the Engine to fail at startup. |
| 119 | + |
| 120 | +### Configure the allowlist with Helm |
| 121 | + |
| 122 | +If you deploy the Engine with the Arcade Helm chart, set the allowlist with `--set`: |
| 123 | + |
| 124 | +```bash |
| 125 | +helm upgrade arcade monorepo/deploy/charts/arcade/ \ |
| 126 | + --set engine.ssrfAllowlist[0]="*.corp.internal" \ |
| 127 | + --set engine.ssrfAllowlist[1]="10.10.0.0/16" |
| 128 | +``` |
| 129 | + |
| 130 | +### Verify the connection |
| 131 | + |
| 132 | +Use the worker test endpoint in the Arcade dashboard. A successful test confirms the allowlist entry is correct and that the Engine has a network path to the MCP server. |
| 133 | + |
| 134 | +## Deploy across multiple networks |
| 135 | + |
| 136 | +For multiple business units or regions, deploy a separate Engine per network. Each Engine has its own `ssrf_allowlist` scoped to the servers in that network, and all Engines share one Coordinator for identity and access management. |
| 137 | + |
| 138 | +<Image |
| 139 | + alt="Scenario 2: Finance, Engineering, and EU each run their own Arcade Engine in their own private network with its own ssrf_allowlist. All three make outbound-only HTTPS calls to one shared Coordinator, and cross-network access is blocked because the other networks' addresses aren't in each allowlist." |
| 140 | + className="mt-4 h-auto w-full rounded-lg border border-border" |
| 141 | + src="/images/warp-tunnels/scenario-2.svg" |
| 142 | + width={1300} |
| 143 | + height={800} |
| 144 | + unoptimized |
| 145 | +/> |
| 146 | + |
| 147 | +A typical topology assigns one Engine to each network: |
| 148 | + |
| 149 | +| Deployment | Network | `ssrf_allowlist` | MCP servers | |
| 150 | +| --- | --- | --- | --- | |
| 151 | +| `finance-nyc` | NYC datacenter | `10.10.0.0/16`, `*.finance.corp.internal` | Bloomberg, SAP, Workday | |
| 152 | +| `engineering-use1` | AWS `us-east-1` | `10.20.0.0/16`, `*.eng.corp.internal` | GitHub Enterprise, Jira | |
| 153 | +| `eu-euw1` | AWS `eu-west-1` | `10.30.0.0/16`, `*.eu.corp.internal` | EU APIs, GDPR-scoped data | |
| 154 | + |
| 155 | +This topology gives you: |
| 156 | + |
| 157 | +- **Network isolation** — each deployment can only reach the hosts and ranges in its own allowlist. The finance deployment can't dial `10.20.x.x` even if it receives a URI pointing there. |
| 158 | +- **Data residency** — the EU deployment's tool traffic never leaves eu-west-1, and credentials in its vault stay in-region. |
| 159 | +- **Credential isolation** — each deployment holds its own credential vault. A compromise of one deployment exposes no credentials from the others. |
| 160 | +- **Shared identity** — all deployments make outbound HTTPS calls to the same Coordinator for auth and RBAC. The data planes are independent; the control plane is shared. |
| 161 | + |
| 162 | +## Expose the Engine to external AI clients |
| 163 | + |
| 164 | +When the Engine runs inside your private network, external AI clients, such as Claude, ChatGPT, Cursor, or your own agents, need to reach it from the internet without you opening inbound firewall rules. |
| 165 | + |
| 166 | +Run a reverse proxy inside your network. It makes one outbound connection, so no inbound ports are needed. External clients connect to the proxy's public hostname, and the proxy forwards traffic to the Engine internally. |
| 167 | + |
| 168 | +<Image |
| 169 | + alt="Scenario 3: external AI clients on the public internet reach the private Arcade Engine through a reverse proxy (cloudflared, ngrok, or nginx) running inside the network. The proxy makes one outbound connection, so no inbound ports are opened; everything behind it is identical to scenario 1." |
| 170 | + className="mt-4 h-auto w-full rounded-lg border border-border" |
| 171 | + src="/images/warp-tunnels/scenario-3.svg" |
| 172 | + width={1200} |
| 173 | + height={940} |
| 174 | + unoptimized |
| 175 | +/> |
| 176 | + |
| 177 | +All AI clients see the same RFC 9728 OAuth interface regardless of which proxy you use. The security and governance layer is identical whether traffic arrives through cloudflared, ngrok, nginx, or a direct private-network path. |
| 178 | + |
| 179 | +<Tabs items={["Cloudflare Tunnel", "ngrok", "nginx"]}> |
| 180 | +<Tabs.Tab> |
| 181 | + |
| 182 | +[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) runs `cloudflared` inside your network and exposes the Engine on a public hostname. |
| 183 | + |
| 184 | +```yaml filename="config.yaml" |
| 185 | +tunnel: <tunnel-id> |
| 186 | +credentials-file: /etc/cloudflared/credentials.json |
| 187 | +ingress: |
| 188 | + - hostname: arcade.yourdomain.com |
| 189 | + service: http://arcade:9099 |
| 190 | + - service: http_status:404 |
| 191 | +``` |
| 192 | + |
| 193 | +```bash |
| 194 | +cloudflared tunnel run |
| 195 | +``` |
| 196 | + |
| 197 | +External clients connect to `https://arcade.yourdomain.com/mcp/{gateway-slug}`. |
| 198 | + |
| 199 | +</Tabs.Tab> |
| 200 | +<Tabs.Tab> |
| 201 | + |
| 202 | +[ngrok](https://ngrok.com) forwards a public domain to the Engine's port with a single command: |
| 203 | + |
| 204 | +```bash |
| 205 | +ngrok http 9099 --domain arcade.your-org.ngrok.app |
| 206 | +``` |
| 207 | + |
| 208 | +External clients connect to `https://arcade.your-org.ngrok.app/mcp/{gateway-slug}`. |
| 209 | + |
| 210 | +</Tabs.Tab> |
| 211 | +<Tabs.Tab> |
| 212 | + |
| 213 | +nginx works as a straightforward reverse proxy. MCP uses Server-Sent Events (SSE) for streaming, so you must disable buffering. |
| 214 | + |
| 215 | +```nginx filename="arcade.conf" |
| 216 | +server { |
| 217 | + listen 443 ssl; |
| 218 | + server_name arcade.yourdomain.com; |
| 219 | +
|
| 220 | + ssl_certificate /etc/ssl/certs/arcade.crt; |
| 221 | + ssl_certificate_key /etc/ssl/private/arcade.key; |
| 222 | +
|
| 223 | + location / { |
| 224 | + proxy_pass http://arcade:9099; |
| 225 | +
|
| 226 | + # Required for SSE / streaming |
| 227 | + proxy_buffering off; |
| 228 | + proxy_cache off; |
| 229 | + proxy_read_timeout 3600s; |
| 230 | + proxy_http_version 1.1; |
| 231 | + proxy_set_header Connection ''; |
| 232 | +
|
| 233 | + # Required headers |
| 234 | + proxy_set_header Host $host; |
| 235 | + proxy_set_header X-Real-IP $remote_addr; |
| 236 | + proxy_set_header Authorization $http_authorization; |
| 237 | + proxy_set_header Mcp-Session-Id $http_mcp_session_id; |
| 238 | + proxy_set_header MCP-Protocol-Version $http_mcp_protocol_version; |
| 239 | + } |
| 240 | +} |
| 241 | +``` |
| 242 | + |
| 243 | +External clients connect to `https://arcade.yourdomain.com/mcp/{gateway-slug}`. |
| 244 | + |
| 245 | +</Tabs.Tab> |
| 246 | +</Tabs> |
| 247 | + |
| 248 | +### Required proxy headers |
| 249 | + |
| 250 | +Whichever proxy you use, verify these headers pass through unchanged: |
| 251 | + |
| 252 | +| Header | Purpose | What breaks if stripped | |
| 253 | +| --- | --- | --- | |
| 254 | +| `Authorization` | OAuth bearer token | Auth fails; the Engine returns `401` | |
| 255 | +| `Mcp-Session-Id` | Session continuity | Each request starts a new session | |
| 256 | +| `MCP-Protocol-Version` | Protocol negotiation | Connection errors with strict clients | |
| 257 | + |
| 258 | +The path `/mcp/{slug}` must also pass through intact. The Engine routes by gateway slug from the path. |
| 259 | + |
| 260 | +## Client compatibility |
| 261 | + |
| 262 | +| Client | Proxy options | Auth mode | |
| 263 | +| --- | --- | --- | |
| 264 | +| Claude (Messages API or managed agents) | Any | Arcade Auth or User Source only | |
| 265 | +| Cursor, Cline, Claude Desktop | Any | All modes | |
| 266 | +| ChatGPT, OpenAI agents | Any | All modes | |
| 267 | +| Custom agents | Any | All modes | |
| 268 | + |
| 269 | +## Known limitations |
| 270 | + |
| 271 | +| Item | Notes | |
| 272 | +| --- | --- | |
| 273 | +| Arcade Headers with the Anthropic Messages API | Not compatible. The connector supports only `authorization_token`, with no custom headers. Use Arcade Auth or User Source. | |
| 274 | +| nginx SSE buffering | `proxy_buffering off` is required. Without it, nginx buffers SSE events and clients never receive streamed responses. | |
| 275 | +| `Mcp-Session-Id` pass-through | Verify your proxy forwards this header. Stripping it silently breaks session continuity for streamable HTTP clients. | |
| 276 | +| Wildcard apex mismatch | `*.corp.internal` does not match `corp.internal`. Register the apex separately if the Engine needs to reach it. | |
| 277 | +| CIDR with split-horizon DNS | CIDR entries match against the resolved IP. If a hostname resolves to different IPs inside and outside the network, use exact-host or wildcard entries instead. | |
| 278 | + |
| 279 | +## Next steps |
| 280 | + |
| 281 | +- [Configure the Arcade Engine](/guides/deployment-hosting/configure-engine) for the full `engine.yaml` reference |
| 282 | +- [Create an MCP Gateway](/guides/mcp-gateways) to scope tools and auth for each client |
| 283 | +- [Set up a User Source](/guides/user-sources) to authenticate end users with your own identity provider |
| 284 | +- [Connect your MCP client](/get-started/mcp-clients) to a gateway URL |
0 commit comments