Caddy-mcp: tunnel private MCP servers through Caddy over QUIC

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:

  1. 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?

  2. For the “aware” mode, I’m doing JSON-RPC parsing in the transport. Is caddyhttp.Handler middleware a better place for that? I went with transport because the policy decision is per-upstream, but I’m second-guessing.

  3. 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.