Gateway Integration¶
The MCP Lifecycle Operator supports exposing MCP servers through external gateways. This allows clients outside the cluster to reach MCP servers via a shared ingress point, without requiring individual LoadBalancer services per server.
How It Works¶
The gateway integration uses a provider-based design built around the MCPGatewayBinding CRD:
- You configure
spec.gatewayon an MCPServer with a provider name and optional configuration - The operator creates an
MCPGatewayBindingresource - An integration controller watches bindings for its provider and creates the appropriate gateway resources
- The binding status is reflected back into the MCPServer status
graph LR
User[User] -->|configures| MCPServer[MCPServer<br/>spec.gateway]
MCPServer -->|creates| Binding[MCPGatewayBinding]
Binding -->|watched by| Controller[Integration<br/>Controller]
Controller -->|creates| Resources[Provider-specific<br/>Resources]
Resources -->|routes via| Gateway[Gateway /<br/>Ingress / etc.]
Gateway -->|traffic| Service[MCP Server<br/>Service]
This design is extensible - any provider can implement its own integration controller by watching MCPGatewayBinding resources filtered by spec.provider. The operator ships with a reference implementation for the Kubernetes Gateway API.
MCPServer Configuration¶
Add spec.gateway to your MCPServer to enable gateway integration:
apiVersion: mcp.x-k8s.io/v1beta1
kind: MCPServer
metadata:
name: my-mcp-server
namespace: default
spec:
source:
type: ContainerImage
containerImage:
ref: quay.io/containers/kubernetes_mcp_server:latest
config:
port: 8080
path: /mcp
gateway:
provider: httproute
configRef: mcp-gateway-config
| Field | Required | Description |
|---|---|---|
gateway.provider |
Yes | Provider name that identifies the integration controller |
gateway.configRef |
No | Name of a ConfigMap with provider-specific configuration |
MCPGatewayBinding¶
The operator automatically creates an MCPGatewayBinding when spec.gateway is set. This CRD is the contract between the operator and integration controllers:
apiVersion: mcp.x-k8s.io/v1alpha1
kind: MCPGatewayBinding
metadata:
name: my-mcp-server-gateway-binding
namespace: default
spec:
mcpServerRef: my-mcp-server
provider: httproute
configRef: mcp-gateway-config
status:
url: http://mcp.example.com/mcp
conditions:
- type: Registered
status: "True"
Note
You do not create MCPGatewayBinding resources manually - the operator manages their lifecycle based on spec.gateway.
Status¶
When gateway integration is active, the MCPServer status includes:
- A
GatewayRegisteredcondition indicating whether the provider has processed the binding - A
gatewayBindingsection with the binding name and provider - The
address.urloverridden with the gateway endpoint (if the provider sets one)
status:
address:
url: http://mcp.example.com/mcp
gatewayBinding:
name: my-mcp-server-gateway-binding
provider: httproute
conditions:
- type: Accepted
status: "True"
- type: Available
status: "True"
- type: Verified
status: "True"
- type: GatewayRegistered
status: "True"
Removing Gateway Integration¶
Remove spec.gateway from the MCPServer to disable gateway integration:
kubectl patch mcpserver my-mcp-server --type=json \
-p='[{"op": "remove", "path": "/spec/gateway"}]'
The operator deletes the MCPGatewayBinding, the provider's resources are cleaned up via owner references, and the MCPServer address reverts to the cluster-internal service URL.
Reference Provider: httproute¶
The operator includes a reference integration controller for the httproute provider, which creates Gateway API HTTPRoute resources.
Prerequisites¶
- Gateway API CRDs installed on the cluster
- A Gateway resource deployed and managed by a gateway controller (e.g., Envoy Gateway, Istio, Cilium)
Note
The operator checks for the HTTPRoute CRD at startup. If Gateway API CRDs are not installed, the httproute controller is skipped. Install them and restart the operator to enable it.
ConfigMap Format¶
The httproute provider reads its configuration from a ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: mcp-gateway-config
namespace: default
data:
gateway-name: my-gateway
gateway-namespace: gateway-system
hostname: mcp.example.com
| Key | Required | Description |
|---|---|---|
gateway-name |
Yes | Name of the existing Gateway resource |
gateway-namespace |
Yes | Namespace where the Gateway resource lives |
hostname |
No | Hostname to set on the HTTPRoute for routing |
Cross-namespace routing
When the Gateway lives in a different namespace than the MCPServer (as in this example), the Gateway must explicitly allow cross-namespace routes. By default, Gateway API sets allowedRoutes.namespaces.from: Same, which rejects routes from other namespaces. Configure the Gateway listener to accept routes from the MCPServer's namespace:
What It Creates¶
For each registered binding, the controller creates an HTTPRoute that:
- References the specified Gateway as a parent
- Matches the MCPServer's path (default
/mcp) usingPathPrefix - Routes traffic to the MCPServer's Service and port
- Sets the hostname if configured
The HTTPRoute is owned by the MCPGatewayBinding, so it is automatically deleted when the binding is removed.
Verify¶
# MCPGatewayBinding should be Registered
kubectl get mcpgatewaybindings
# HTTPRoute should exist
kubectl get httproutes
# MCPServer address should reflect the gateway URL
kubectl get mcpserver my-mcp-server -o jsonpath='{.status.address.url}'
Implementing a Custom Provider¶
Any provider needs to:
- Create a controller that watches
MCPGatewayBindingresources filtered byspec.provider - Read configuration from the ConfigMap referenced by
spec.configRef - Create provider-specific resources owned by the binding (for automatic cleanup)
- Update the binding status with a
Registeredcondition and optionally setstatus.url
The MCPServer controller reflects the binding status automatically - your provider only needs to manage the MCPGatewayBinding status, not the MCPServer status directly.
Adding an In-Tree Provider¶
The operator uses a provider registry so that cmd/main.go does not need per-provider setup code. To add a new in-tree provider:
- Create a package under
internal/controller/providers/<name>/ -
Implement a
Reconcilerwith aSetupWithManager(mgr ctrl.Manager) errormethod that usesproviders.MatchesProviderto filter bindings by provider name:package myprovider import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" mcpv1alpha1 "github.com/kubernetes-sigs/mcp-lifecycle-operator/api/v1alpha1" "github.com/kubernetes-sigs/mcp-lifecycle-operator/internal/controller/providers" ) const ProviderName = "myprovider" func init() { providers.Register(ProviderName, Setup) } func Setup(mgr ctrl.Manager) error { return (&Reconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr) } func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&mcpv1alpha1.MCPGatewayBinding{}, builder.WithPredicates(providers.MatchesProvider(ProviderName))). // Owns(...), Watches(...), etc. Complete(r) } -
Add a blank import in
cmd/main.go:
The providers.SetupAll(mgr) call in cmd/main.go handles the rest.
Adding an Out-of-Tree Provider¶
An out-of-tree provider runs as a separate controller in its own deployment. It watches MCPGatewayBinding resources filtered by its provider name and manages its own resources independently. No changes to the operator are required - the provider needs RBAC access to watch and read MCPGatewayBinding resources, update their status, read referenced ConfigMap resources, and manage its provider-specific resources.