Proposal: thv-cloudflare — an operator-style control plane for deploying MCP servers to Cloudflare Containers
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
Problem / motivation
ToolHive deploys containerized MCP servers to two places today: Docker-compatible local runtimes and Kubernetes (via thv-operator + thv-proxyrunner). There is no path for teams that want managed, scale-to-zero container hosting without operating a cluster. Consuming Cloudflare-hosted MCPs already works via thv run <url> / MCPRemoteProxy — the gap is deployment, lifecycle, and edge governance.
Cloudflare Containers run arbitrary OCI images, controlled by a Worker + Durable Object, with on-demand start, sleepAfter idle shutdown, and global ingress — a good match for the MCP usage pattern (intermittent, request-driven, HTTP transports).
No open-source tool today bridges an MCP container catalog to a serverless container platform, in the user's own cloud account, with governance middleware in front. ToolHive has every ingredient — registry, CRD-based declarative API, auth/authz/audit stack, vMCP aggregation — so it is well placed to be first.
This issue proposes a single new component and is exploratory: feedback wanted before any code; a full RFC in stacklok/toolhive-rfcs can follow.
Prior art (and why it doesn't cover this)
MCP hosting and gateways
- Cloudflare Agents SDK /
McpAgent— official framework for writing remote MCP servers in JS/TS on Workers; not a deployer for existing container images. - Glama hosting — closest product shape (container in, gateway-fronted MCP endpoint out), but proprietary SaaS on their infrastructure.
- Composio / MintMCP (managed SaaS gateways), Smithery (registry only).
- Docker MCP Gateway, Obot, Microsoft MCP Gateway, IBM ContextForge, MCPJungle — self-hosted peers, all Docker/K8s targets, none serverless.
- Fly.io / Cloud Run / Render — generic container hosts; hand-written deploy config, no MCP-aware lifecycle or middleware.
Declarative control planes for Cloudflare
The pattern exists — for infrastructure, not applications:
- Crossplane
provider-upjet-cloudflare— ~190 Cloudflare resources (Workers scripts, KV, R2, DNS, Zero Trust) as Kubernetes CRDs reconciled against the Cloudflare API. Validates exactly the Git-to-Cloudflare reconcile loop proposed here, but it requires a Kubernetes cluster to host the reconciler — defeating the "no cluster" goal — and it is resource-level plumbing with no notion of an application. - Community
cloudflare-operatorprojects (adyanth, 0ekk, jacaudi) — K8s operators for tunnels, Zero Trust, DNS, R2; none target Workers Containers. Notably, jacaudi's operator drives everything through the officialcloudflare-goSDK — precedent that the pure-Go API mechanism proposed here is viable. - Alchemy — TypeScript-native IaC covering Workers and containers; a library you run, not a control plane, and TS-only.
- Cloudflare Workers Builds — native push-to-deploy CI for a single Worker repo; per-repo build automation, not fleet reconciliation.
The gap
Existing tools are either MCP-aware but cluster-bound, or Cloudflare-native but resource-level. Nothing is application-level for MCP — understanding a server as a unit with a transport, an auth/authz policy, an egress profile, and an aggregation topology — while running cluster-free. thv-cloudflare would relate to Crossplane's Cloudflare provider the way thv-operator relates to Crossplane on Kubernetes: app semantics vs. raw infrastructure, complementary rather than competing.
Proposal: a dedicated thv-cloudflare control plane
Rather than growing thv itself (a deploy command, or a new runtime backend inside the CLI), all Cloudflare logic lives in one new binary, cmd/thv-cloudflare/ — following the repo's established multi-binary pattern (thv, thv-operator, thv-proxyrunner, vmcp). The Kubernetes parallel, explicitly:
| Kubernetes | Cloudflare analogue |
|---|---|
thv-operator watches CRDs in the API server |
thv-cloudflare reads CRD-shaped YAML from Git |
| Reconciles Deployments/StatefulSets | Reconciles Workers + Containers via the CF REST API |
thv-proxyrunner as in-cluster data plane |
vMCP / Worker shim as the data plane |
kubectl |
thv cloudflare ... subcommands |
Declarative spec
The operator's existing CRD schemas (MCPServer, VirtualMCPServer, MCPGroup, MCPServerEntry, MCPToolConfig, …) as plain YAML files in a Git repo. The CRD Go types (cmd/thv-operator/api/v1alpha1, v1beta1) are plain structs importable without controller-runtime or an API server — spec parsing is literal code reuse, and the Kubernetes and Cloudflare surfaces stay identical by construction.
One reconciler core, three run modes
thv-cloudflare --once— CI/GitOps mode, the proposed v1: runs on push, diffs desired (Git) vs. actual (Cloudflare API), converges, exits. Nothing always-on to host.- Daemon mode — long-running reconciler hosted anywhere, polling Git or receiving webhooks.
- Self-hosted on Cloudflare — the end-state: the reconciler runs as a Cloudflare Container in the account it manages, woken by a Cron Trigger or Git webhook. Possible because v1 needs no Docker daemon (below). The whole system — control plane, vMCP gateway, MCP servers — lives in one Cloudflare account.
Honest limitation vs. thv-operator: no watch stream — drift-correction latency equals the poll/webhook cadence rather than being near-instant.
Mechanism and images
- Pure Go against the Cloudflare REST API (Worker upload, container-app registration, registry interactions). No Wrangler/Node dependency. Reimplementing this under-documented deploy plumbing is the proposal's main engineering cost.
- v1 images: pre-built OCI images only, mirrored from their registry (e.g. ghcr.io) to
registry.cloudflare.comin pure Go (go-containerregistry) — no Docker anywhere in the control plane. Protocol schemes (uvx://,npx://,go://) are future work, e.g. built from the existing Dockerfile templates (pkg/container/templates/) by the same CI job that runs--once.
Topology and state
- Standalone servers: one Worker + Container each (
thv-<name>) — independent lifecycle, own URL; removing the YAML removes the Worker. - Groups: the vMCP-fronted shared Worker (next section).
- State: the Cloudflare API is the source of truth — resources enumerated by naming convention, ToolHive labels in Worker metadata. Status and day-2 reads surface through
thv cloudflare ...subcommands attaching to the component's API (same pattern asthv serve); deployed endpoints can optionally be registered as ToolHive remote workloads for local consumption.
Security (day one, not deferred)
- Public Worker endpoints ship a TS shim performing real JWT/OIDC validation via JWKS — the same tokens ToolHive's auth middleware verifies — before any request reaches a container.
- Worker-side egress enforcement: Cloudflare Containers route outbound HTTP through the Worker (
interceptOutboundHttp), so permission-profile egress allowlists are enforced at the edge — isolation parity with the local Squid/dnsmasq design by different means. Non-mappable profile aspects (volume mounts, capabilities) fail fast with explicit errors. - Credentials:
CLOUDFLARE_API_TOKENenv in CI mode; ToolHive's secrets providers when running attached tothv.
How Virtual MCP fits (recommended multi-server shape)
vMCP (cmd/vmcp/, pkg/vmcp/) is arguably the most Cloudflare-deployable component ToolHive has: a standalone Go HTTP server (streamable-http/SSE), no stdio, no Docker socket — it runs as a Cloudflare Container with zero porting. Cloudflare supports multiple container classes per Worker, so a group deploys as a single shared Worker:
graph LR
Client[MCP Client] -->|HTTPS + OIDC| W[Shared Worker<br/>thin router]
W --> V[vMCP Container<br/>incoming auth, authz,<br/>aggregation, audit]
V --> B1[MCP Container A]
V --> B2[MCP Container B]
V -->|outgoing auth / token exchange| Ext[MCPServerEntry<br/>remote backend anywhere]
- Backends are unreachable from the internet by construction — only vMCP gets a public route; per-backend auth reduces to trivial service-to-service checks.
- Full middleware parity without porting — vMCP already centralizes incoming OIDC, authz, tool filtering, audit, and outgoing auth (token exchange, header injection). Compiling ToolHive's Go middleware to WASM in the Worker becomes a future optimization (edge execution while containers sleep; deps are pure Go and WASM-compatible, but the 10 MB compressed script limit and TinyGo's reflection gaps need a spike), not a prerequisite.
- Hybrid for free:
MCPServerEntrylets a Cloudflare-hosted vMCP front backends living in Kubernetes, other clouds, or SaaS. - Trade-offs: group membership changes redeploy the shared Worker; vMCP's health/discovery polling would wake sleeping containers, so the design needs lazy discovery (cache capabilities, refresh on demand or on deploy) to preserve scale-to-zero economics.
Alternatives considered
thv deploy cloudflare <server>command: fastest to ship (shelling out to Wrangler), but it grows the core CLI surface, adds Node + Docker as dependencies ofthv, and has no lifecycle story (no list/logs/drift). Set aside in favor of keeping Cloudflare logic out of the core binary.- A
cloudflareruntime backend (TOOLHIVE_RUNTIME=cloudflare): fullthv run/list/logsparity via theRuntimeinterface, but it embeds the Cloudflare API plumbing — and its failure modes — inside everythvinstallation, and its imperative single-server model duplicates what the declarative component does better. Set aside.
Both remain buildable later on top of the component's internals (pkg/cloudflare/) if demand appears — pushback welcome if maintainers would rather start smaller.
Cross-cutting scope
- Transports: v1 targets SSE / streamable-http servers only — Cloudflare Containers are HTTP-ingress by design. This excludes most stdio-only registry servers at first; the planned follow-up is a derived image baking ToolHive's existing stdio-to-HTTP proxy (
pkg/transport/proxy/) into the container as an entrypoint bridge. - Platform constraints to document: Workers Paid plan requirement, container instance/resource limits, scale-to-zero cold starts.
Future work (not v1 scope)
- Protocol-scheme servers (
uvx://,npx://,go://): built from the existing Dockerfile templates by the same CI job that runs--once. - stdio bridge image: derived image baking ToolHive's stdio-to-HTTP proxy as entrypoint, unlocking the rest of the registry.
- WASM middleware in the Worker: edge-native execution while containers sleep, pending the size/TinyGo spike.
- Web dashboard: the component already exposes an API for
thv cloudflare ...subcommands, so a UI is another client of it (precedent: ToolHive Studio over thethv serveAPI). In the Cloudflare-hosted mode this becomes an SPA served from Workers assets, protected by Cloudflare Access, living in the same account it manages. Git remains the single write path — the UI edits manifests or opens PRs rather than mutating Cloudflare directly, so it never fights the reconciler.
Open questions
- Is a fifth binary (
cmd/thv-cloudflare) acceptable to the project, or should the reconciler start life asthvsubcommands and split out once daemon/Cloudflare-hosted modes land? - Is reusing the CRD schemas as plain YAML outside Kubernetes acceptable, or does a Cloudflare-native spec make more sense?
- How should vMCP's discovery/health polling adapt to scale-to-zero backends (lazy discovery, cached capabilities, wake-on-demand)?
- Is the WASM middleware spike worth prioritizing as an optimization once the vMCP topology lands — and what's the fallback if TinyGo cannot compile
cedar-go? - Should Cloudflare Access / Zero Trust service tokens be offered as an alternative edge-auth mode alongside OIDC-at-edge?
- Where should platform limits and pricing guidance live (docs/arch, or registry entries)?
References
- Cloudflare Containers: https://developers.cloudflare.com/containers/
- Durable Object Container API (incl.
interceptOutboundHttp): https://developers.cloudflare.com/durable-objects/api/container/ - Image management / Cloudflare registry: https://developers.cloudflare.com/containers/platform-details/image-management/
- ToolHive runtime contract:
docs/runtime-implementation-guide.md - Operator architecture (the K8s analogue):
docs/arch/09-operator-architecture.md - Virtual MCP architecture:
docs/arch/10-virtual-mcp-architecture.md
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the existing CRD types under cmd/thv-operator/api/v1alpha1 and v1beta1, then inspect cmd/vmcp/ and pkg/vmcp/ to understand the proposed reuse. This issue is exploratory and names no implementation tests or settled acceptance criteria; done currently means resolving the open design questions and agreeing on an RFC and v1 scope.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, go, kubernetes
- Domain
- backend-api-design, cloud, devops
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100