Francis Du.

💻Data Engineer | 🦀 Rustacean | 📷 Photographer | 🤖Vibe Coder

23 Sep 2026

A Stable MCP Tunnel for wcode: Cloudflare or Tailscale Funnel

On this page10 sections

wcode can create its own public tunnel, and for a temporary repository that is still the easiest path.

For something I use every day, I do not want the MCP address to change whenever the tunnel is recreated. A hostname change means updating the client and changing the OAuth resource origin as well.

So I now keep long-lived wcode instances behind a stable tunnel endpoint. I use both Cloudflare Tunnel and Tailscale Funnel:

ChatGPT / Claude / other MCP client
               │
               │ stable HTTPS /mcp
               â–¼
    Cloudflare Tunnel / Tailscale Funnel
               │
               â–¼
        127.0.0.1:8765
               │
               â–¼
             wcode

wcode still stays on loopback. I do not open a router port or expose the local MCP server to the LAN.

The only thing wcode needs is the stable public HTTPS origin.

That is what –public-url is for.

The local wcode side barely changes#

The current Remote MCP server listens on:

127.0.0.1:8765

by default.

If cloudflared runs on the same machine, I point the Tunnel origin directly at:

http://127.0.0.1:8765

I do not change wcode to 0.0.0.0 just because I am using a tunnel. Cloudflare can reach the loopback service locally, so there is no reason to expose the MCP server to the rest of the LAN.

Assume the stable hostname is:

https://mcp.example.com

I start wcode with:

wcode --public-url https://mcp.example.com

and configure the AI client with:

https://mcp.example.com/mcp

–public-url does not disable wcode’s OAuth layer. It tells wcode that an external stable entry point already exists, so wcode should use that origin instead of starting a random managed tunnel.

This is also how I reuse the model quota I already have in ChatGPT#

One of the practical reasons I built wcode was simple: if I am already using ChatGPT in the browser, I do not want the same coding task to require a second API bill just so the model can touch my local repository.

A stable Remote MCP path gives me this shape:

ChatGPT
   │
   │ Remote MCP
   â–¼
https://stable-host/mcp
   │
   â–¼
wcode
   │
   â–¼
local repository

This is not literally free compute.

If I already pay for a ChatGPT plan or have model usage available there, the model is still being paid for through ChatGPT. What I avoid is sending the same workflow through the OpenAI API and paying separately by API token. That is the part I care about.

There is an important product limitation to keep honest.

As of September 23, 2026, OpenAI’s public documentation says ChatGPT connects to remote MCP servers rather than directly to a localhost endpoint. Pro users can connect MCPs with read/fetch permissions in Developer Mode, while full write/modify MCP support is still rolling out for Business, Enterprise, and Edu.

So whether ChatGPT can let wcode actually edit code depends on the current plan and workspace permissions. That matrix will change over time.

The network requirement does not: ChatGPT needs a reachable, stable Remote MCP origin.

That is the problem the tunnel solves.

I prefer the Cloudflare-managed tunnel for this#

Cloudflare currently recommends remotely-managed tunnels for most cases. The configuration lives on Cloudflare, and the machine only needs a Tunnel token to run the connector.

On macOS:

brew install cloudflared

Then in the Cloudflare dashboard:

Networking
  → Tunnels
  → Create Tunnel

I normally just name it wcode.

Add a Published application route:

Hostname
  mcp.example.com

Service
  http://127.0.0.1:8765

The domain must already be on Cloudflare.

One useful detail: when the hostname is added from the Tunnel’s Published application screen, Cloudflare creates the DNS routing for it. I do not need to create a second hand-written CNAME for the normal full-DNS setup.

The dashboard then gives me the connector command. It looks roughly like this:

sudo cloudflared service install <TUNNEL_TOKEN>

The token is a credential. I do not put it in a repository, an issue, or a terminal screenshot.

Once the connector is online:

wcode --public-url https://mcp.example.com

That is enough to give wcode a stable public MCP origin.

The fully local config is still useful#

Sometimes I want the entire Tunnel configuration on the machine, especially if the same connector publishes several local services. In that case I use a locally-managed tunnel.

Authenticate:

cloudflared tunnel login

Create a named tunnel:

cloudflared tunnel create wcode

That creates a Tunnel UUID and a credentials file under something like:

~/.cloudflared/<TUNNEL-UUID>.json

Then I create ~/.cloudflared/config.yml:

tunnel: <TUNNEL-UUID>
credentials-file: /Users/your-name/.cloudflared/<TUNNEL-UUID>.json

ingress:
  - hostname: mcp.example.com
    service: http://127.0.0.1:8765

  - service: http_status:404

The final 404 rule matters. An ingress configuration needs a catch-all rule at the end.

I validate the file before starting anything:

cloudflared tunnel ingress validate

Create the DNS route:

cloudflared tunnel route dns wcode mcp.example.com

Run the tunnel:

cloudflared tunnel run wcode

and start wcode separately:

wcode --public-url https://mcp.example.com

At that point the path is stable from the client all the way to the local runtime.

On macOS I let cloudflared stay running#

A fixed hostname is less useful if I still have to remember to launch the tunnel manually.

For a locally-managed tunnel, macOS can install cloudflared as a per-user LaunchAgent:

cloudflared service install

That uses:

~/.cloudflared/config.yml

and starts when I log in, which is usually what I want on a development machine.

There is also a boot-level LaunchDaemon:

sudo cloudflared service install

The important difference is that the system service expects its configuration under /etc/cloudflared, not the user’s home directory. This is an easy way to end up wondering why a perfectly good ~/.cloudflared/config.yml suddenly cannot be found after adding sudo.

For my laptop, the login LaunchAgent is simpler.

Tailscale Funnel is even less configuration#

If the machine already runs Tailscale, Funnel is the other path I like.

Cloudflare Tunnel is better when I want my own domain. Tailscale Funnel is better when I just want a stable public HTTPS endpoint without touching DNS.

The important limitation is that Funnel does not let me choose an arbitrary custom hostname. Its public DNS name lives under the tailnet’s ts.net domain.

Once the machine is logged into Tailscale, I can publish wcode’s local port in the background:

tailscale funnel --bg 8765

That proxies:

http://127.0.0.1:8765

to a public HTTPS name similar to:

https://my-mac.my-tailnet.ts.net

I can inspect the actual endpoint with:

tailscale funnel status

Then I start wcode with that origin:

wcode --public-url https://my-mac.my-tailnet.ts.net

and give the MCP client:

https://my-mac.my-tailnet.ts.net/mcp

Tailscale describes Funnel as a way to expose a local service through a predictable, stable ts.net HTTPS name. The machine and Tailscale still need to be online, but there is no separate DNS record or Cloudflare Tunnel token to manage.

On first use, the CLI may ask me to enable the tailnet requirements for Funnel, including HTTPS and the relevant Funnel permission.

Funnel is public internet access. If I only want the service reachable inside my tailnet, the right command is tailscale serve, not Funnel.

I check /healthz before debugging OAuth, whichever tunnel I use#

Cloudflare:

curl -sS https://mcp.example.com/healthz | jq

Tailscale:

curl -sS https://my-mac.my-tailnet.ts.net/healthz | jq

A healthy response includes ok: true and identifies the current wcode instance.

This splits debugging into two smaller problems:

/healthz fails
    → Tunnel / DNS / origin problem

/healthz works, MCP client fails
    → OAuth / client interoperability problem

Before OAuth is complete, this:

curl -i https://mcp.example.com/mcp

can return 401 Unauthorized.

That does not mean the tunnel is broken. wcode’s MCP endpoint is supposed to be protected. The tunnel provides reachability; it does not bypass authentication.

One tunnel can publish more than wcode#

A single Cloudflare Tunnel can route several hostnames:

ingress:
  - hostname: mcp.example.com
    service: http://127.0.0.1:8765

  - hostname: dashboard.example.com
    service: http://127.0.0.1:3000

  - hostname: api.example.com
    service: http://127.0.0.1:8080

  - service: http_status:404

I still give wcode its own hostname instead of hiding it under a path on another application.

wcode has more than /mcp: OAuth discovery and authorization endpoints, token handling, health checks, and the MCP transport all share the same public origin. A dedicated hostname removes a whole class of proxy path-rewrite problems.

The stable public origin is the actual requirement#

wcode does not care whether that origin comes from Cloudflare or Tailscale.

These are the two shapes I use:

Cloudflare Tunnel
https://mcp.example.com

Tailscale Funnel
https://my-mac.my-tailnet.ts.net

Then I pass the matching origin to wcode:

wcode --public-url https://mcp.example.com

or:

wcode --public-url https://my-mac.my-tailnet.ts.net

As long as the public origin forwards the complete wcode surface without breaking HTTPS, Host handling, OAuth metadata, or the MCP path, the rest of the runtime is the same.

Cloudflare is what I use when I want my own domain. Tailscale Funnel is what I use when the machine is already in my tailnet and I want the shortest path to a stable HTTPS address.

I still keep Quick Tunnel for disposable sessions.

For a long-lived instance, especially one connected to a browser model such as ChatGPT, I would rather configure the MCP URL once and stop touching it.

References#