caddy-mcp is a Caddy plugin I’ve been working on. It tunnels MCP (Model Context Protocol) servers from private networks to the public side — no inbound firewall rules. The private box dials out over QUIC, Caddy presents Streamable HTTP (and SSE for long-running responses) to MCP clients on the public side.
Architecture:
Claude Desktop → Caddy (HTTPS) → caddy-mcp plugin (QUIC listener) ↑ private MCP server (rift client, dial-out)
It’s built on top of rift, which handles the QUIC dial-out + multiplexing. The plugin runs a QUIC listener (currently on a separate port from Caddy’s main listener) and registers a reverse_proxy transport that forwards JSON-RPC over the active client tunnels.
Two modes per tunnel:
-
Transparent — Caddy forwards bytes untouched, MCP server handles its own auth/session
-
Aware — Caddy parses the JSON-RPC and applies tool/resource ACLs before forwarding (
allow_tools,deny_tools,allow_resources)
A few things I’d love feedback on from people who know Caddy internals better than I do:
-
The QUIC listener currently runs on its own port. Would it make sense to hook into Caddy’s native HTTP/3 listener instead, or does that get messy because the QUIC streams here aren’t HTTP/3 framed?
-
For the “aware” mode, I’m doing JSON-RPC parsing in the transport. Is
caddyhttp.Handlermiddleware a better place for that? I went with transport because the policy decision is per-upstream, but I’m second-guessing. -
Anyone here actually deploying MCP servers behind Caddy yet? Curious what patterns you’ve landed on.
Repo: https://github.com/venkatkrishna07/caddy-mcp
Plugin is WIP, MIT-licensed, feedback genuinely welcome.