Isolated MCP Gateway Deployment¶
This guide demonstrates how to deploy MCP Gateway instances for your environment. Each deployment is given its own configuration for MCP Servers to manage based on the MCPGatewayExtension resource. This allows for multiple MCP Gateway instances to be deployed within a single cluster and to isolate traffic.
This guide assumes some knowledge about exposing MCP servers via an HTTPRoute. You can find more info in the following guide configure-mcp-gateway-listener.
This guide assumes some knowledge about configuring an MCPServerRegistration. You can find more information in the following guide register-mcp-servers.
Overview¶
The MCP Gateway requires an MCPGatewayExtension resource to operate. This resource:
- Defines which Gateway the MCP Gateway instance is responsible for
- Determines where configuration secrets are created (same namespace as the MCPGatewayExtension)
- Enables isolation by allowing multiple MCP Gateway instances (in different namespaces) to target different Gateways
Prerequisites¶
- Cluster with Gateway API support
- Istio installed as the Gateway API provider
- Helm 3.x
Note: The guide expects you have cloned the repo locally. This allows using the latest code. If you want to make use of a specific release, use the following in the helm commands and then there is no need for the repo locally:
helm upgrade -i mcp-controller oci://ghcr.io/kuadrant/charts/mcp-gateway \
--version ${MCP_GATEWAY_VERSION} \
Step 1: Install MCP Gateway CRDs¶
export MCP_GATEWAY_VERSION=main #change this to the version you want
kubectl apply -k "https://github.com/kuadrant/mcp-gateway/config/crd?ref=${MCP_GATEWAY_VERSION}"
Verify the CRDs are installed:
Note: CRDs are also installed automatically when deploying the controller via Helm in Step 3.
Architecture¶
┌──────────────────────────────────────────────────────────────────────┐
│ MCP System Namespace │
├──────────────────────────────────────────────────────────────────────┤
│ MCP Controller (cluster-wide) │
└──────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ Team A NS │ │ Team B NS │
├───────────────────────────────┤ ├───────────────────────────────┤
│ MCPGatewayExtension │ │ MCPGatewayExtension │
│ → Gateway A │ │ → Gateway B │
│ MCP Broker/Router │ │ MCP Broker/Router │
│ Config Secret │ │ Config Secret │
│ MCPServerRegistrations │ │ MCPServerRegistrations │
└───────────────────────────────┘ └───────────────────────────────┘
Each MCPGatewayExtension must target a different Gateway. The controller creates configuration secrets in the same namespace(s) as valid MCPGatewayExtension(s), which are mounted into the broker/router deployments.
For cross-namespace Gateway references with a MCPGatewayExtension, a ReferenceGrant must exist in the target Gateway's namespace.
Step 2: Set Environment Variables¶
OpenShift¶
export TEAM_A_HOST="team-a.apps.$(oc get dns cluster -o jsonpath='{.spec.baseDomain}')"
export TEAM_B_HOST="team-b.apps.$(oc get dns cluster -o jsonpath='{.spec.baseDomain}')"
Kind/Kubernetes¶
What is shown below is just an example and what is used locally via Kind.
Step 3: Deploy the MCP Controller¶
The MCP Controller runs cluster-wide and reconciles MCPGatewayExtension and MCPServerRegistration resources. Deploy it once in a central namespace:
helm upgrade -i mcp-controller ./charts/mcp-gateway \
--namespace mcp-system \
--create-namespace \
--set controller.enabled=true \
--set gateway.create=false \
--set httpRoute.create=false \
--set mcpGatewayExtension.create=false \
--set envoyFilter.create=false
Verify the controller is running:
Step 4: Deploy Team A Gateway Instance¶
Deploy an MCP Gateway instance for Team A with its own Gateway resource:
helm upgrade -i team-a-mcp-gateway ./charts/mcp-gateway \
--namespace team-a \
--create-namespace \
--set controller.enabled=false \
--set broker.create=true \
--set gateway.create=true \
--set gateway.name=team-a-gateway \
--set gateway.namespace=gateway-system \
--set gateway.publicHost="$TEAM_A_HOST" \
--set gateway.internalHostPattern="*.team-a.mcp.local" \
--set httpRoute.create=true \
--set mcpGatewayExtension.create=true \
--set mcpGatewayExtension.gatewayRef.name=team-a-gateway \
--set mcpGatewayExtension.gatewayRef.namespace=gateway-system \
--set envoyFilter.create=true \
--set envoyFilter.name=team-a-gateway
The Helm chart creates:
- Gateway resource in gateway-system namespace
- HTTPRoute for the MCP endpoint
- MCPGatewayExtension targeting the Gateway
- ReferenceGrant in the Gateway namespace (for cross-namespace references)
- Broker/Router deployment
- Service for the broker
- EnvoyFilter to route traffic through the external processor
- ServiceAccount and RBAC
- Config Secret for MCP server configuration
Step 5: Verify Team A Deployment¶
# Check Gateway is created
kubectl get gateway team-a-gateway -n gateway-system
# Check MCPGatewayExtension is ready
kubectl wait --for=condition=Ready mcpgatewayextension/team-a-mcp-gateway -n team-a --timeout=60s
# Check pods are running
kubectl get pods -n team-a
Step 6: Deploy Team B Gateway Instance¶
Deploy a second isolated gateway for Team B:
helm upgrade -i team-b-mcp-gateway ./charts/mcp-gateway \
--namespace team-b \
--create-namespace \
--set controller.enabled=false \
--set broker.create=true \
--set gateway.create=true \
--set gateway.name=team-b-gateway \
--set gateway.namespace=gateway-system \
--set gateway.publicHost="$TEAM_B_HOST" \
--set gateway.internalHostPattern="*.team-b.mcp.local" \
--set httpRoute.create=true \
--set mcpGatewayExtension.create=true \
--set mcpGatewayExtension.gatewayRef.name=team-b-gateway \
--set mcpGatewayExtension.gatewayRef.namespace=gateway-system \
--set envoyFilter.create=true \
--set envoyFilter.name=team-b-gateway
Step 7: Verify Team B Deployment¶
# Check Gateway is created
kubectl get gateway team-b-gateway -n gateway-system
# Check MCPGatewayExtension is ready
kubectl wait --for=condition=Ready mcpgatewayextension/team-b-mcp-gateway -n team-b --timeout=60s
# Check broker pod is running
kubectl get pods -n team-b
Step 8: Expose Gateways Externally¶
OpenShift (Routes)¶
OpenShift Routes expose the Gateways externally with TLS termination.
Team A gateway route:
oc apply -f - <<EOF
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: team-a-gateway
namespace: gateway-system
spec:
host: $TEAM_A_HOST
tls:
insecureEdgeTerminationPolicy: Redirect
termination: edge
port:
targetPort: mcp
to:
kind: Service
name: team-a-gateway-istio
weight: 100
wildcardPolicy: None
EOF
Team B gateway route:
oc apply -f - <<EOF
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: team-b-gateway
namespace: gateway-system
spec:
host: $TEAM_B_HOST
tls:
insecureEdgeTerminationPolicy: Redirect
termination: edge
port:
targetPort: mcp
to:
kind: Service
name: team-b-gateway-istio
weight: 100
wildcardPolicy: None
EOF
Verify routes are created:
Exposing via NodePort¶
For a local kind or kubernetes setup you can configure helm to setup the NodePort service. Re-run the commands with the following flags set
Next Steps: Register MCP Servers¶
Once your gateway instances are deployed, register MCP servers with each instance. See the Register MCP Servers guide for detailed instructions.
When registering servers, ensure the HTTPRoute's parentRef targets the correct Gateway:
- Team A instance:
team-a-gatewayingateway-system - Team B instance:
team-b-gatewayingateway-system
Limitations¶
Broker/Router must be co-located with MCPGatewayExtension: The MCP Gateway instance (broker and router) must be deployed in the same namespace as the MCPGatewayExtension. The controller writes the configuration secret to the MCPGatewayExtension's namespace, and the broker/router mount this secret.
One MCPGatewayExtension per namespace: Each namespace can only have one MCPGatewayExtension. The controller writes configuration to a well-known secret name, so multiple extensions would overwrite each other.
One MCPGatewayExtension per Gateway: Only one MCPGatewayExtension can target a given Gateway. If multiple extensions target the same Gateway, the controller marks newer ones as conflicted. The oldest extension (by creation timestamp) wins.
Troubleshooting¶
MCPGatewayExtension shows RefGrantRequired¶
The MCPGatewayExtension is targeting a Gateway in a different namespace, but no ReferenceGrant exists:
Look for the condition:
conditions:
- type: Ready
status: "False"
reason: ReferenceGrantRequired
message: "ReferenceGrant required in namespace gateway-system to allow cross-namespace reference"
The Helm chart should create the ReferenceGrant automatically. If not, create it manually as shown in the Manual Resource Creation section.
MCPGatewayExtension shows InvalidMCPGatewayExtension¶
The target Gateway doesn't exist or there's a conflict.
Check if there is another MCPGatewayExtension that is older that is also targeting the Gateway.
MCPServerRegistration shows NotReady¶
The registration can't find a valid MCPGatewayExtension for the Gateway its HTTPRoute is attached to:
MCPServerRegistration resources are only processed when a valid MCPGatewayExtension exists for the Gateway their HTTPRoute is attached to. Without a matching MCPGatewayExtension, registrations will show a NotReady status.
Check that:
- The HTTPRoute exists and references the correct Gateway and its status is accepted
- An MCPGatewayExtension exists targeting that Gateway
- The MCPGatewayExtension is in Ready state
No configuration in secret¶
The config secret exists but has no servers:
Check that MCPServerRegistration resources exist and are Ready:
Cleanup¶
To remove the deployments:
OpenShift¶
# Delete routes
oc delete route team-a-gateway team-b-gateway -n gateway-system
# Uninstall gateway Helm releases
helm uninstall team-b-mcp-gateway -n team-b
helm uninstall team-a-mcp-gateway -n team-a
# Uninstall controller
helm uninstall mcp-controller -n mcp-system
# Delete namespaces
oc delete namespace team-b
oc delete namespace team-a
oc delete namespace mcp-system
Kind/Kubernetes¶
# Uninstall gateway Helm releases
helm uninstall team-b-mcp-gateway -n team-b
helm uninstall team-a-mcp-gateway -n team-a
# Uninstall controller
helm uninstall mcp-controller -n mcp-system
# Delete namespaces
kubectl delete namespace team-b
kubectl delete namespace team-a
kubectl delete namespace mcp-system
Manual Resource Creation¶
If you prefer to create the MCPGatewayExtension and ReferenceGrant manually instead of having Helm manage them, disable automatic creation and apply the resources yourself:
ReferenceGrant (Cross-Namespace Only)¶
If the MCPGatewayExtension is in a different namespace than the Gateway, create a ReferenceGrant in the Gateway's namespace:
kubectl apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-team-a
namespace: gateway-system
spec:
from:
- group: mcp.kagenti.com
kind: MCPGatewayExtension
namespace: team-a
to:
- group: gateway.networking.k8s.io
kind: Gateway
EOF
MCPGatewayExtension¶
Create the MCPGatewayExtension to associate the team's namespace with the target Gateway:
kubectl apply -f - <<EOF
apiVersion: mcp.kagenti.com/v1alpha1
kind: MCPGatewayExtension
metadata:
name: team-a-gateway
namespace: team-a
spec:
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: team-a-gateway
namespace: gateway-system
EOF
Deploy without automatic resource creation¶
helm install team-a-mcp-gateway ./charts/mcp-gateway \
--namespace team-a \
--set controller.enabled=false \
--set gateway.create=true \
--set gateway.name=team-a-gateway \
--set gateway.namespace=gateway-system \
--set gateway.publicHost="$TEAM_A_HOST" \
--set httpRoute.create=true \
--set envoyFilter.create=true \
--set envoyFilter.name=team-a-gateway \
--set mcpGatewayExtension.create=false