MCPG
Reference
Reference

Operator CRD reference

The eight mcpg.dev/v1alpha2 Custom Resource Definitions the MCPG Kubernetes operator reconciles — scope, key spec fields, and status, derived directly from the operator API types and the rendered CRDs.

The MCPG operator manages everything through Custom Resources in the mcpg.dev API group. This page is the authoritative reference for all eight CRDs, their scope, and their key spec fields. For the gateway's own configuration schema (the spec.config payload on an MCPGGateway), see the Configuration reference. To install the operator, follow the Kubernetes install guide.

At a glance

KindShort nameScopePurpose
MCPGGatewaymcpgwNamespacedA gateway Deployment + Service + ConfigMap (+ optional Ingress, NetworkPolicy, PDB, HPA, ServiceMonitor).
MCPGPluginSetmcpgpsNamespacedAn ordered bundle of plugin references with per-plugin runtime config, referenced by a gateway.
MCPGRoutemcpgrNamespacedA per-tenant route into a shared gateway (soft multi-tenancy).
MCPGPluginmcpgpClusterA single signed, OCI-published plugin artefact plus its trust policy.
MCPGRevocationListmcpgrlClusterCluster-wide list of revoked plugin SHA-256 hashes, fanned out to gateways.
MCPGClustermcpgcClusterA shared coordination backend (Redis / NATS / Consul / etcd) binding.
MCPGTenantmcpgtClusterA declarative tenant boundary: owned namespaces, plugin allowlist, quotas.
MCPGPluginMirrormcpgmClusterAn in-cluster OCI mirror declaration for air-gapped plugin pulls.

All eight live in API group mcpg.dev at version mcpg.dev/v1alpha2, which is the served and stored version. Namespaced CRDs scope to a tenant's namespace; cluster-scoped CRDs describe shared infrastructure (signed plugins, revocations, coordinators, mirrors, tenant boundaries) that the platform team owns.

Admission and serving model

The operator runs a single validating admission webhook — there is no mutating webhook and no conversion webhook. Pre-1.0 the operator serves only v1alpha2; older alpha versions are dropped wholesale rather than carried via a conversion webhook, so your manifests should target mcpg.dev/v1alpha2 directly.

  • WebhookValidatingWebhookConfiguration covering all eight kinds on CREATE / UPDATE. The webhook server listens on :9443 inside the operator pod (the Service that kube-apiserver dials defaults to port 443). Default failurePolicy: Fail (fail-closed): when the operator is unreachable, CRD edits are blocked. A namespace label mcpg.dev/skip-validation opts a namespace out of the namespaced webhooks.
  • Metrics / health — the operator's /metrics, /healthz, and /readyz endpoints listen on :8443.

Because the operator is otherwise schema-blind for gateway config (see MCPGGateway.spec.config below), the webhook validates CRD-level invariants — plugin trust, capability-grant subsets, namespace exclusivity, replica caps, anchored cosign regexes — while the gateway's own boot-time validator remains the source of truth for the inner gateway config.


MCPGGateway

Scope: Namespaced · short name mcpgw

The core resource: the operator renders one MCPGGateway into a Deployment + Service + ConfigMap, plus optional Ingress, NetworkPolicy, PodDisruptionBudget, HorizontalPodAutoscaler, ServiceMonitor, and PrometheusRule, all reconciled into the gateway's namespace.

Key spec fields

FieldTypeNotes
imageobjectrepository, tag, pullPolicy — all default at reconcile time (pullPolicy to IfNotPresent).
replicasintDefaults to 1. More than one replica requires a shared coordinator — see clusterRef.
configobjectThe gateway's own AppConfig, verbatim (snake_case keys, deny_unknown_fields). Lands in the rendered ConfigMap. See the Configuration reference.
resourcesobjectrequests / limits maps, mapped to core/v1.ResourceRequirements.
serviceobjecttype, port, annotations.
ingressobjectingressClassName, hosts[], tls[], annotations.
imagePullSecretslistLocalObjectReferences.
workloadIdentityobjectExactly one of aws (iamRoleArn), gcp (googleServiceAccount), azure (clientId), spiffe (trustDomain + svid). Setting more than one is an admission error.
schedulingobjectnodeSelector, tolerations, affinity, topologySpreadConstraints, priorityClassName, terminationGracePeriodSeconds.
probesobjectPer-probe overrides for liveness / readiness / startup.
networkPolicyobjectenabled + extraIngressFrom / extraEgressTo. Default policy denies all but the listen port and management traffic.
podDisruptionBudgetobjectenabled, minAvailable / maxUnavailable (int-or-string, e.g. "50%" or 2).
autoscalingobjectHPA shape: enabled, minReplicas, maxReplicas, metrics[]. Disabled by default.
monitoringobjectserviceMonitor and prometheusRule toggles (require the Prometheus Operator CRDs).
podAnnotations / podLabelsmapStamped onto the rendered pod template.
pluginSetRefobject{ name } — reference to a same-namespace MCPGPluginSet. Cross-namespace sets are deliberately unsupported.
revocationListRefobject{ name } — reference to a cluster-scoped MCPGRevocationList. Defaults to the operator's cluster-default list when unset.
clusterRefobject{ name } — reference to a cluster-scoped MCPGCluster. When unset the gateway runs the in-process single_node coordinator (correct only for a single replica).
acceptedRouteNamespaceslistSoft-tenancy allow-list of namespaces permitted to attach an MCPGRoute to this gateway. Empty ⇒ no external routes accepted (routes in the gateway's own namespace are always allowed).

Status

conditions[] is the authoritative readiness signal (Ready, Available). Hash fields let you confirm a change has propagated: configHash (SHA-256 of the rendered config, flips on any spec change that reaches the pod template), pluginSetHash, and revocationListHash. Replica counters (replicas, readyReplicas, updatedReplicas, availableReplicas) mirror the Deployment.

yaml
apiVersion: mcpg.dev/v1alpha2
kind: MCPGGateway
metadata:
  name: dev-gateway
  namespace: mcpg-dev
spec:
  replicas: 1
  pluginSetRef:
    name: minimal-plugins
  config:
    gateway:
      server:
        bind_address: "0.0.0.0:8787"
  resources:
    requests: { cpu: 100m, memory: 128Mi }
    limits:   { cpu: 500m, memory: 512Mi }

MCPGPluginSet

Scope: Namespaced · short name mcpgps

An ordered bundle of plugin references with per-plugin runtime config. A gateway references one set via spec.pluginSetRef; the set names cluster-scoped MCPGPlugin resources (a cross-scope read the operator validates for RBAC and readiness at admission and reconcile time).

Key spec fields

FieldTypeNotes
entrieslistOne per plugin. Order is preserved in the rendered gateway config — order-sensitive chains (identity → policy → audit) rely on it.
entries[].idstringPlugin id, e.g. dev.mcpg.identity.workload. Must match the referenced MCPGPlugin.spec.pluginId.
entries[].pluginRefobject{ name } — reference to a cluster-scoped MCPGPlugin, which must be Ready before the set converges.
entries[].enabledboolDefaults to true. Disabled entries pass through to status but are skipped when rendering config and materialising Secrets.
entries[].enforceboolDefaults to true. false runs the plugin in shadow mode: it evaluates, logs, and emits metrics, but Deny/Challenge decisions are mapped to Allow. Maps to PluginEntryConfig.enforce.
entries[].configobjectInline per-plugin runtime config, passed through verbatim to the gateway's plugins.entries[].config. The operator does not validate it (the gateway's boot-time validators do); marked x-kubernetes-preserve-unknown-fields.
capabilityGrantsmapKeyed by plugin id; each value is a subset of that plugin descriptor's required_capabilities. The webhook validates the subset relationship.

Status

resolvedEntries == totalEntries is the canonical Ready signal. resolvedHash is the SHA-256 of the rendered config (entries + per-entry configs + grants) so dependent gateways can detect change and roll pods. Per-entry failures land in failedEntries[] with a stable reason (PluginNotFound, PluginNotReady, PluginRevoked, PluginIdMismatch, ArtefactSecretMissing).

yaml
apiVersion: mcpg.dev/v1alpha2
kind: MCPGPluginSet
metadata:
  name: minimal-plugins
  namespace: mcpg-dev
spec:
  entries:
    - id: dev.mcpg.builtin.audit
      pluginRef:
        name: builtin-audit        # required — a cluster-scoped MCPGPlugin that must be Ready
      enabled: true
      config:
        sinks:
          - type: stdout

MCPGRoute

Scope: Namespaced · short name mcpgr

A per-tenant route into a shared gateway — the soft multi-tenancy path. From their own namespace, a tenant team declares which tools they expose through a gateway owned by the platform team, plus the chains and per-tenant attributes that govern them. MCPGRoute is the only CRD permitted to reference a gateway in another namespace, and only when that gateway opts in via MCPGGateway.spec.acceptedRouteNamespaces.

Key spec fields

FieldTypeNotes
gatewayRefobject{ name, namespace? } — the gateway this route attaches to. For soft tenancy, namespace points at the shared-gateway namespace; it defaults to the route's own namespace when unset.
match.toolslist[{ id }] — tools this route exposes. Each must correspond to a binding the gateway actually serves (validated at admission).
identityChainlistOrdered identity-plugin ids. Validated against the gateway's plugin set.
policyChainlistOrdered policy-engine plugin ids. Same validation caveat.
auditChainlistOrdered audit-sink plugin ids. Same validation caveat.
attributesmapPer-tenant metadata. attributes.tenant is special: it is the identity attribute the rendered tool-access CEL rules key on. All attributes are available to audit sinks for labelling.

What is enforced today

The operator fans each route's match.tools + attributes.tenant into the shared gateway's governance.policy.tool_access.rules[] as tenant-scoped CEL rules (a tool is reachable only when $identity.attributes.tenant == "<tenant>"). That is enforced at tools/list (visibility) and tools/call (authorization).

The identityChain / policyChain / auditChain fields are validated and recorded, but per-route chain dispatch is not yet enforced by the gateway runtime — a route cannot today swap the identity/policy/audit chain on a per-tool basis. The controller reports this honestly via the ChainsEnforced status condition (False, reason PerRouteDispatchUnsupported). The tool-access scoping still applies. See the Multi-tenant deployments guide.

Status

Notable conditions: Ready (route accepted, tool-access rules rendered), GatewayBound (referenced gateway exists and accepts this route's namespace), and ChainsEnforced (today False). matchedTools counts the rendered rules; boundGateway is the resolved <namespace>/<name>.


MCPGPlugin

Scope: Cluster · short name mcpgp

A single OCI-published, signed plugin artefact plus the trust policy under which the operator verifies it. Cluster-scoped because the same plugin bytes are byte-identical across every namespace — storing once cluster-wide deduplicates. Tenant MCPGPluginSet resources reference plugins by name.

Key spec fields

FieldTypeNotes
pluginIdstringPlugin id matching the descriptor embedded in the artefact, e.g. dev.mcpg.identity.workload. Verified on pull.
versionstringShould match the OCI tag's semver component; mismatches are rejected.
pluginClassstringAdvisory routing hint, re-checked against the descriptor. Values follow PluginClass: identity_provider, policy_engine, credential_issuer, audit_sink, cluster_backend, binding, transport.
oci.imagestringFull OCI ref. Tag-form is allowed for development; production should pin a digest (...@sha256:...).
oci.pullSecretRefobjectOptional pull-secret in the operator's namespace.
oci.mirrorRefobjectOptional { name } reference to an MCPGPluginMirror — pull through the in-cluster mirror instead of the public registry.
trust.signingKeyRefobject{ secretName, key } — Ed25519 public-key Secret in the operator's namespace. Mandatory; key defaults to release.pub.
trust.cosignIdentityobjectOptional cosign keyless trust: certificateIdentityRegexp (must be anchored with ^$; unanchored patterns are rejected) + oidcIssuer.
trust.slsaProvenanceobjectOptional SLSA L3 in-toto pin: configMapName, sourceUri, sourceTag.

The three trust layers are fail-closed and run in order: Ed25519 signature (mandatory) → cosign keyless (optional) → SLSA L3 provenance (optional). The signing-key Secret must live in the operator's namespace so a tenant cannot supply its own trust anchor.

Status

resolvedDigest is the SHA-256 of the verified cdylib bytes (lower-case hex). signatureValid / cosignVerified / slsaVerified are tri-state (Option<bool>: absent ⇒ not yet evaluated). revokedBySha flips true when resolvedDigest matches the cluster MCPGRevocationList; the operator refuses to materialise a revoked plugin into any set. artefactSecretName names the operator-managed Secret holding the verified bytes.


MCPGRevocationList

Scope: Cluster · short name mcpgrl

A cluster-wide list of revoked plugin SHA-256 hashes. The operator treats a single resource named cluster-default as authoritative and fans it out into every namespace running a gateway as a per-namespace ConfigMap, mounted read-only into the gateway pod. Gateway pods enforce at plugin-load time — a list update plus a pod roll is the full revocation cycle.

Key spec fields

FieldTypeNotes
versionintFormat version. Defaults to 1; unknown versions are rejected at admission.
issuedAtstringRFC3339 audit-only timestamp; freshness is not gated on it.
revocationslistOne entry per revoked artefact.
revocations[].artifactSha256string64-char lower-case hex of the verified cdylib bytes (post-cosign-extract), not the OCI manifest digest. The webhook normalises uppercase and rejects duplicates.
revocations[].reasonstringFree-form; surfaced in the gateway's load-time error and audit event. Empty/whitespace-only reasons are rejected.
revocations[].revokedAtstringRFC3339 timestamp, audit-only.

Status

observedRevocations is the last-observed entry count; materialisedNamespaces counts the namespaces holding a materialised ConfigMap; pluginsBlocked[] lists the MCPGPlugin resources flagged revokedBySha; contentHash lets you verify rollout consistency across consumer namespaces.


MCPGCluster

Scope: Cluster · short name mcpgc

A shared coordination-backend binding. A multi-replica gateway needs a shared coordinator for sessions, leases, pub/sub, and idempotency state. MCPGCluster centralises that: the platform team declares the backend once, and any gateway binds it with clusterRef: { name: ... }. The operator renders the backend's cluster: config block and ensures the matching dev.mcpg.cluster.<kind> cdylib entry is present.

Key spec fields

FieldTypeNotes
backendenumsingle_node (default, in-process — no external dependency, valid only for one replica), redis, nats, consul, or etcd.
configmapPer-backend configuration, rendered verbatim into the gateway's cluster: block (e.g. url, key_prefix for redis; servers, bucket for nats). Schema-blind: the gateway's own validator is the source of truth. Ignored for single_node.
pluginRefobjectOptional { name } reference to the cluster cdylib's source MCPGPlugin. When set, the operator requires it to be Ready (verified, not revoked) before binding.
credentialRefslist[{ name, secretName, key? }] — backend credentials projected into bound gateway pods as cred://cluster/<name>, resolved at config-load time. Keeps secrets out of the world-readable spec.

The backend enum maps to cdylib ids: redisdev.mcpg.cluster.redis, natsdev.mcpg.cluster.nats, consuldev.mcpg.cluster.consul, etcddev.mcpg.cluster.etcd. single_node loads no cdylib.

Status

pluginId is the resolved cdylib id (absent for single_node). boundGateways counts gateways currently bound via clusterRef (blast-radius signal before an edit). configHash is the SHA-256 of the rendered cluster: block, which bound gateways fold into their pod-roll hash so a cluster config edit re-rolls every bound gateway.

yaml
apiVersion: mcpg.dev/v1alpha2
kind: MCPGCluster
metadata:
  name: prod-redis
spec:
  backend: redis
  config:
    url: "redis://redis.mcpg-prod.svc.cluster.local:6379"
    key_prefix: "mcpg.prod"
  credentialRefs:
    - name: password
      secretName: redis-creds
      key: password

MCPGTenant

Scope: Cluster · short name mcpgt

A declarative tenant boundary. A cluster-admin declares the namespaces a tenant owns, which cluster MCPGPlugins those namespaces may reference, and hard quotas. Cluster-scoped because only a cluster-admin should define tenant boundaries — a tenant must not be able to grant itself more namespaces or raise its own quota. Namespaces are exclusively owned (a namespace belongs to at most one tenant, enforced at admission). Opt-in: a cluster with no MCPGTenant keeps the implicit, PluginSet-triggered RBAC unchanged.

Key spec fields

FieldTypeNotes
namespaceslistExisting namespaces this tenant owns. The operator does not create them — it references existing namespaces.
allowedPluginslistPlugin allowlist. Empty list = deny-all (a tenant must explicitly opt in); {name: "*"} = any cluster plugin. Each entry matches by name (resource name or capability id) OR by registryPrefix (the plugin image's registry prefix).
quotasobjectHard caps: maxGateways, maxPluginSets, maxRoutes (count quotas via a generated per-namespace ResourceQuota), maxReplicasPerGateway (a field constraint enforced at admission). Any unset field = unlimited.
identityAttributeobjectOptional { key, value }. Stamps a consistent $identity.attributes.<key> == "<value>" predicate into the tenant's gateway tool_access rules so its gateways and routes share one identity boundary. When unset, MCPGTenant renders no gateway config (pure RBAC + admission object).

Division of labour

The admission webhook synchronously enforces the plugin allowlist, namespace exclusivity, and the per-gateway replica cap. The reconcile loop (eventual) binds per-namespace Secret-write RoleBindings, labels namespaces mcpg.dev/tenant=<name>, and generates the per-namespace ResourceQuota — the race-safe count-quota enforcement (the apiserver holds the lock; the webhook count-check is only a nicer error message).

Status

boundNamespaces[] lists the namespaces successfully bound (exist + labelled + RBAC/quota applied). observed carries aggregate counts (gateways, pluginSets, routes) for quota-headroom visibility only — never the enforcement point. Notable conditions: Ready, NamespacesBound, and QuotaWithinLimits (a soft signal, not a gate).


MCPGPluginMirror

Scope: Cluster · short name mcpgm

An in-cluster OCI mirror declaration for air-gapped plugin pulls. It is a rewrite rule + endpoint descriptor, not the registry itself and not a pull-through cache (images must be pre-mirrored by a sync station). An MCPGPlugin opts in via spec.oci.mirrorRef; the operator then rewrites the upstream ref onto the mirror and never falls back to the public registry (fail-closed).

Key spec fields

FieldTypeNotes
endpoint.serviceobject{ namespace, name, port, pathPrefix? } — the in-cluster Service hosting the mirror registry. The rewrite target is <name>.<namespace>.svc.cluster.local:<port>[/<pathPrefix>].
endpoint.insecureboolWhen true, the mirror is treated as plain-HTTP / self-signed. In-cluster mirrors on :80 are the common case.
upstream.registrystringPublic registry hostname this mirror stands in for, e.g. ghcr.io.
upstream.namespacestringNamespace/org path under the registry, e.g. mcpg-dev/source-code. Only refs starting with <registry>/<namespace> are rewritten; others are left untouched.
auth.secretRefobjectOptional { secretName, key? } — a dockerconfigjson Secret in the operator namespace, used for mirror pulls. key defaults to .dockerconfigjson.
resyncIntervalstringOptional reachability re-check cadence hint (e.g. 1h); informational.

The tag and @sha256: digest pin are preserved through the rewrite, so the content-addressed identity the operator verifies is unchanged. cosign cert-identity and SLSA source-URI checks still validate against the upstream repo — a mirror cannot launder an unsigned artefact.

Status

reachable reports whether the operator could reach the mirror's /v2/ endpoint at last reconcile (false ⇒ pulls through this mirror will fail). proxiedReferences counts MCPGPlugins referencing this mirror. endpointHost is the resolved in-cluster registry host.

yaml
apiVersion: mcpg.dev/v1alpha2
kind: MCPGPluginMirror
metadata:
  name: airgap-mirror
spec:
  endpoint:
    service:
      namespace: oci-mirror
      name: harbor
      port: 80
      pathPrefix: /v2/mirror
    insecure: true
  upstream:
    registry: ghcr.io
    namespace: mcpg-dev/source-code
  auth:
    secretRef:
      secretName: mirror-pull
      key: .dockerconfigjson
  resyncInterval: 1h

Cross-references