You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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-operator projects (adyanth, 0ekk, jacaudi) — K8s operators for tunnels, Zero Trust, DNS, R2; none target Workers Containers. Notably, jacaudi's operator drives everything through the official cloudflare-go SDK — 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.
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.com in 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 as thv 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_TOKEN env in CI mode; ToolHive's secrets providers when running attached to thv.
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]
Loading
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:MCPServerEntry lets 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 of thv, and has no lifecycle story (no list/logs/drift). Set aside in favor of keeping Cloudflare logic out of the core binary.
A cloudflare runtime backend (TOOLHIVE_RUNTIME=cloudflare): full thv run/list/logs parity via the Runtime interface, but it embeds the Cloudflare API plumbing — and its failure modes — inside every thv installation, 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 the thv serve API). 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 as thv subcommands 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)?
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 viathv 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,
sleepAfteridle 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-rfcscan follow.Prior art (and why it doesn't cover this)
MCP hosting and gateways
McpAgent— official framework for writing remote MCP servers in JS/TS on Workers; not a deployer for existing container images.Declarative control planes for Cloudflare
The pattern exists — for infrastructure, not applications:
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.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.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-cloudflarewould relate to Crossplane's Cloudflare provider the waythv-operatorrelates to Crossplane on Kubernetes: app semantics vs. raw infrastructure, complementary rather than competing.Proposal: a dedicated
thv-cloudflarecontrol planeRather than growing
thvitself (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:thv-operatorwatches CRDs in the API serverthv-cloudflarereads CRD-shaped YAML from Gitthv-proxyrunneras in-cluster data planekubectlthv cloudflare ...subcommandsDeclarative 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.Honest limitation vs.
thv-operator: no watch stream — drift-correction latency equals the poll/webhook cadence rather than being near-instant.Mechanism and images
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
thv-<name>) — independent lifecycle, own URL; removing the YAML removes the Worker.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)
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.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]MCPServerEntrylets a Cloudflare-hosted vMCP front backends living in Kubernetes, other clouds, or SaaS.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.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
pkg/transport/proxy/) into the container as an entrypoint bridge.Future work (not v1 scope)
uvx://,npx://,go://): built from the existing Dockerfile templates by the same CI job that runs--once.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
cmd/thv-cloudflare) acceptable to the project, or should the reconciler start life asthvsubcommands and split out once daemon/Cloudflare-hosted modes land?cedar-go?References
interceptOutboundHttp): https://developers.cloudflare.com/durable-objects/api/container/docs/runtime-implementation-guide.mddocs/arch/09-operator-architecture.mddocs/arch/10-virtual-mcp-architecture.md