Skip to main content

Command Palette

Search for a command to run...

Postgres MCP & Cloudflare MCP Portal is a cheat-code!

Why MCP + Cloudflare is a Cheat Code for creative developers

Updated
6 min read
L

I've discovered coding back in 2013 and three years later I spent all my summer building my first Laravel app which is still in production by the non-profit I've built for.

Now I'm struggling to find the balance between enjoying the power of "I can build this myself" and not chocking myself to death trying to build everything myself.

As it is common for developers to be less articulate, I decided to leverage writing about my endeavours, to keep me up.

There are so many useful MCP servers these days and some of them are critical between the matter of "can an AI agent produce quality output and achieve task to my satisfaction or cannot".

AI is like a genius, but often with no - or very limited - hands (tools).

The Problem: Remote Postgres MCP isn’t Straightforward

Remote Postgres MCP with Cloudflare Tunnel

In this case, I often work with PostgreSQL databases (my favorite actually) and I began using postgres-mcp quite a lot. The source repo is at https://github.com/crystaldba/postgres-mcp. Unfortunately it seems like the project has fallen behind and is not really maintained.

It works fine when using locally but my goal was to have an effortless remote mcp for my remote Neon database. For a small niche side‑hustle I have a free tier Neon pg instance that stores valuable data for me. That’s something you don’t expose to the entire world.

My Setup: Docker + Cloudflare Tunnel

I went ahead and created a mini docker compose project pairing the cloudflare tunnel and the postgres-mcp:

services:
  # MCP server (SSE transport) using forked version of crystaldba/postgres-mcp.
  # Reach it from the host at {your cloudflare tunnel public URL}/mcp
  mcp:
    # image: crystaldba/postgres-mcp
    image: ghcr.io/flexchar/postgres-mcp:latest
    restart: unless-stopped
    environment:
      - DATABASE_URI=${NEON_PG_DSN}
    command:
      [
        "--access-mode=restricted",
        "--transport=streamable-http",
        "--streamable-http-port=8000",
        "--streamable-http-host=0.0.0.0",
      ]
    networks:
      - internal

  # Cloudflare tunnel (deploy only)
  # https://hub.docker.com/r/cloudflare/cloudflared
  cloudflared:
    image: cloudflare/cloudflared:latest # pick the one most recent
    restart: unless-stopped
    # https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/configure-tunnels/cloudflared-parameters/run-parameters/
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
    networks:
      - internal

networks:
  internal:

This lets me deploy both services on my server without exposing anything to the web. These days security is being extra challenged and I sleep better knowing that the attack surface is as tiny as possible. In this case, no one knows the IP of my server, furthermore, it doesn't even matter because the server itself doesn't respond to any requests. It's all inside to you, not the other way around.

A few worthy mentions:

  • I also have a Makefile for deployment helpers that uses docker --context server to via SSH to deploy the compose project.

  • Then the .env for storing the key variables that I can provide using 1password secrets (thus if I installed nasty npm package, so those secrets never touch the raw filesystem).

  • Finally, the docker image is stored on my GitHub because the upstream does not have official Streamable HTTP implementation. I used the PR from Ahmed Mustahid: https://github.com/crystaldba/postgres-mcp/pull/78 to build the docker image and push to my ghrc.io so I can pull easily.

Locking it down: Zero Trust + Hidden Infrastructure

Now, inside Cloudflare I have setup the tunnel exposing the MCP under one of my domains paired together with an app in Cloudflare Zero Trust. This way, no one can access the tunnel, and I am pretty damn sure no one can hack it either (if one can hack cloudflare so we'd have a greater problem than my humble database).

The Pain: Cloudflare Access Tokens Are Too Much Work

All of this works, but only when connecting from Claude Desktop or Codex ... so why not on the web without generating custom Cloudflare Access Service Tokens... (hint: missing OAuth callback urls but let's dive step by step).

Easy authentication using Cloudflare MCP and Managed OAuth.

While tinkering around, it was easy to notice the portal feature. I quickly began playing with it. It's still in BETA status. It took me a few hours spread out across a few days to comprehend how it works.

Official guide at: https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/secure-mcp-servers/ (I don't care about passing the auth token up-stream to the worker since it's only me using anyways).

Bad news, is that once one adds Zero Trust Application in front of the tunnel, the dashboard reports as the MCP non-responsive.

Good news, it still works however one has to configure auth part before the Zero Trust. This is confusing. Meaning you create the tunnel, deploy to VPS, expose and it works with wide open to the internet.

Step‑by‑Step: How to Configure MCP Auth Correctly

Then, before creating Zero Trust app on your chosen domain, one must go to AI Controls (menu item on the left under Access Controls) -> MCP Servers (tab) -> Add a new MCP server -> Select Auth to None (or what fits you and create no Access Policies).

Soon after, you should be able to see tools listed by the server, this confirms it works! Yay. Now we create the Access Control by going to the classic Access Controls -> Applications tab and select the same subdomain that you just created an MCP server on. It's on https://dash.cloudflare.com/~/one/access-controls/apps (~ = placeholder for your account id).

If there is interest, I might create a video on this because it is quite confusing.

Once again, we do not create any access policies on the MCP server, we create them using classic Zero Trust -> Applications on the same subdomain that we just exposed our MCP server.

Bingo, now we are ready to pair with Claude on the web, right?

The Gotcha: Why Claude Shows ‘Authorization Failed’

We go to https://claude.ai/customize/connectors?modal=add-custom-connector type our subdomain-to-domain.tld/mcp as a URL, click add and get the lovely error of

"Authorization with the MCP server failed. You can check your credentials and permissions. If this persists, share this reference with support: “ofid_random_alphanumeric_string....”

Dammit. Cloudflare just can't make it easy, can it?

Well, kind of :)

There is an open ticket on Cloudflare forum from ronenmars but only crickets there... the TLS/SSL issue wasn't the case for me:

(image from the forum thread but I saw the same error)

after a bit of debugging and adhd brain, I found it.

Enable Managed OAuth + Register Callback URLs

We need to go to the Zero Trust -> Application for our MCP server -> Open it -> Advanced Settings (tab) -> Managed OAuth (scroll to the bottom section). Make sure it is enabled (as per official guide above) and we need to registered callback URLs. There are two key ones:

for Claude Connectors: https://claude.ai/api/mcp/auth_callback

for ChatGPT Apps: https://chatgpt.com/connector/oauth/*

Sweet, now it works!

Now I can put my mcp URL and effortlessly connect from Claude on the web, on the desktop, same with the custom registered ChatGPT Apps option (the equivalent to the custom connector in Claude, required developer mode to be enabled).

The client (such as Claude) will take care of exchanging tokens with Cloudflare Zero Trust behind the scenes. It is a bit messy and a bit of tinkering but if I were to do this from scratch, it wouldn't take more than 5 to 15 minutes!

Given all the value Cloudflare provides for free, the extra setup is worth it. If you are reading this (and not your AI agent), let me know if you had the same roadblock!