A Kubernetes controller for managing MCP (Model Context Protocol) servers through custom resource definitions.
This controller defines and manages two custom resources:
- MCPServerPolicy (cluster-scoped): Defines policies for MCP server deployments including deployment type, proxy configuration, allowed namespaces, and server images.
- MCPServerRequest (namespace-scoped): Represents a request to deploy an MCP server instance based on a policy.
The controller reconciles MCPServerRequest resources by:
-
Policy Lookup: Fetches the MCPServerPolicy referenced by
spec.policyRef- Updates status with error if policy not found
-
Namespace Validation: Checks if the request namespace is in the policy's
allowedNamespaceslist- Empty list means all namespaces are allowed
- Updates status with error if namespace not allowed
-
Resource Creation: Based on the policy's deployment type and proxy type:
Exclusive + Toolhive:
- Creates a Toolhive MCPServer CR in the same namespace as the request
- Configures it with policy settings (image, env, args, resources, OIDC, authz, audit, tools)
Shared + Kuadrant:
- Creates a Deployment in the shared namespace with the MCP server container
- Creates a Service to expose the deployment
- Creates a Kuadrant MCPServer CR that references an HTTPRoute
Remote:
- No resources created, uses the remote URL from policy
-
Status Updates: Updates MCPServerRequest status with:
- Phase (Pending, Running, Failed, Terminating)
- Endpoint URL for accessing the server
- Conditions reflecting policy found, namespace allowed, and ready state
- ObservedGeneration for tracking changes
-
Policy Changes: When a policy is updated, the controller automatically updates owned resources to match the new policy configuration.
Depending on your deployment and proxy configuration, you'll need:
- For Toolhive proxy: Toolhive operator installed
- For Kuadrant proxy: Kuadrant MCP Gateway installed
- Gateway API (for Kuadrant): HTTPRoute CRDs installed
mcp-asset-management/
├── api/v1alpha1/ # API type definitions
│ ├── groupversion_info.go # API group and version
│ ├── mcpserverpolicy_types.go
│ └── mcpserverrequest_types.go
├── cmd/ # Application entry point
│ └── main.go
├── config/crd/bases/ # Generated CRD manifests
├── controllers/ # Controller implementations
│ ├── mcpserverpolicy_controller.go
│ └── mcpserverrequest_controller.go
├── hack/ # Build scripts and tools
│ └── boilerplate.go.txt
├── Dockerfile # Container image definition
├── Makefile # Build targets
└── go.mod # Go module definition
- Go 1.22+
- Kubernetes cluster (for deployment)
- kubectl configured to access your cluster
- Install dependencies:
go mod download- Generate CRDs and code:
make manifests generate- Build the controller:
make buildGenerate CRD manifests from Go types:
make manifestsGenerate DeepCopy methods:
make generateRun tests:
make testRun the controller locally:
make runInstall CRDs to your cluster:
make installNote: The config/crd/bases directory contains generated CRDs for both this controller's resources and external resources (Toolhive and Kuadrant MCPServers). Only install the MCPServerPolicy and MCPServerRequest CRDs from this project. The external MCPServer CRDs should be installed by their respective operators:
mcp.opendatahub.io_mcpserverpolicies.yaml- Install thismcp.opendatahub.io_mcpserverrequests.yaml- Install thismcp.opendatahub.io_toolhivemcpservers.yaml- DO NOT install (comes from Toolhive operator)mcp.opendatahub.io_kuadrantmcpservers.yaml- DO NOT install (comes from Kuadrant operator)
Build and push container image:
make docker-build docker-push IMG=<registry>/mcp-controller:tagUninstall CRDs:
make uninstallCluster-scoped resource defining MCP server deployment policies.
The deployment field is a discriminated union supporting three deployment types:
Dedicated MCP server deployment with full configuration control:
apiVersion: mcp.opendatahub.io/v1alpha1
kind: MCPServerPolicy
metadata:
name: exclusive-policy
spec:
serverImage: mcp-server:v1.0.0
deployment:
type: Exclusive
exclusive:
serviceAccount: mcp-server-sa
args: ["--verbose"]
env:
- name: LOG_LEVEL
value: "debug"
labels:
app: mcp-server
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
allowedNamespaces:
- default
- production
resourceLimits:
maxInstances: 5Shared MCP server deployment running in a specific namespace:
apiVersion: mcp.opendatahub.io/v1alpha1
kind: MCPServerPolicy
metadata:
name: shared-policy
spec:
serverImage: mcp-server:v1.0.0
deployment:
type: Shared
shared:
namespace: mcp-shared
serviceAccount: shared-mcp-sa
args: ["--mode=shared"]
resources:
requests:
memory: "512Mi"
cpu: "250m"
allowedNamespaces:
- team-a
- team-bAccess to an external MCP server via URL:
apiVersion: mcp.opendatahub.io/v1alpha1
kind: MCPServerPolicy
metadata:
name: remote-policy
spec:
serverImage: mcp-server:v1.0.0
deployment:
type: Remote
remote:
url: https://mcp-server.external.example.com:8443/api
allowedNamespaces:
- defaultNamespace-scoped resource requesting an MCP server deployment.
apiVersion: mcp.opendatahub.io/v1alpha1
kind: MCPServerRequest
metadata:
name: my-mcp-server
namespace: default
spec:
policyRef: example-policy
replicas: 2
configuration:
key1: value1
key2: value2
serviceAccountName: mcp-server-saCreate a policy for exclusive deployment with Toolhive proxy:
kubectl apply -f config/samples/exclusive-policy.yamlCreate a request using this policy:
kubectl apply -f config/samples/exclusive-request.yamlThe controller will:
- Validate the policy exists
- Check that
defaultnamespace is in the allowed list - Create a Toolhive MCPServer CR in the
defaultnamespace - Update the MCPServerRequest status with the endpoint URL
Check the status:
kubectl get mcpserverrequest my-exclusive-mcp -n default -o yamlExpected status:
status:
phase: Running
endpoint: http://my-exclusive-mcp.default.svc.cluster.local:9090
conditions:
- type: PolicyFound
status: "True"
reason: PolicyFound
- type: NamespaceAllowed
status: "True"
reason: NamespaceAllowed
- type: Ready
status: "True"
reason: ResourcesCreatedCreate a policy for shared deployment with Kuadrant:
kubectl apply -f config/samples/shared-policy.yamlCreate a request using this policy:
kubectl apply -f config/samples/shared-request.yamlThe controller will:
- Validate the policy and namespace
- Create a Deployment in the
mcp-sharednamespace - Create a Service to expose the deployment
- Create a Kuadrant MCPServer CR
- Update the status with the endpoint
Check created resources:
kubectl get deployment,service -n mcp-shared
kubectl get kuadrantmcpserver -n team-aCreate a request with a non-existent policy:
apiVersion: mcp.opendatahub.io/v1alpha1
kind: MCPServerRequest
metadata:
name: bad-request
namespace: default
spec:
policyRef: non-existent-policyThe controller will update the status:
status:
phase: Failed
conditions:
- type: PolicyFound
status: "False"
reason: PolicyNotFound
message: "Policy non-existent-policy not found"
- type: Ready
status: "False"
reason: ReconciliationFailedmake help- Display available targetsmake build- Build the controller binarymake manifests- Generate CRD manifestsmake generate- Generate code (DeepCopy, etc.)make test- Run testsmake install- Install CRDs to clustermake uninstall- Remove CRDs from clustermake run- Run controller locallymake docker-build- Build container imagemake docker-push- Push container image
Copyright 2024.
Licensed under the Apache License, Version 2.0.