Self-hosting
Self-hosting6 min

Multi-tenant deployments

Serve multiple teams or customers from one MCPG gateway — tenant identity from claims, per-tenant tool allowlists, rate limits, and hard isolation via the operator's MCPGTenant CRD.

MCPG has no top-level tenants: block by design. Per-tenant behaviour composes from primitives that already exist: tenant identity from the verified principal, a per-tenant tool allowlist in the policy gate, and a per-tenant rate-limit plugin. For cluster-level hard isolation on Kubernetes, the operator adds the MCPGTenant CRD.

When you need this

  • A SaaS company giving each customer their own slice of one gateway
  • A platform team running MCPG for multiple internal teams
  • A consultant or MSP managing several client deployments

A single team running one gateway needs none of this — the default config serves everyone the same tool set.

The three application-layer primitives

Start from the multi-tenant template:

bash
mcpg config init --template multi-tenant --output config.yaml
mcpg config check config.yaml

1. Tenant identity comes off the verified principal

Whichever OIDC claim represents your tenant (commonly tid, org_id, or a custom claim) lands under identity.attributes.<claim> once the inbound JWT verifier resolves it. No config knob is needed for the tenant id itself — wire the bearer verifier and reference the claim:

yaml
governance:
  access:
    oidc_oauth:
      providers:
        - issuer: "https://example.okta.com"
          audiences: ["mcpg-gateway"]
          verification:
            kind: oidc_jwks
            allowed_algs: ["RS256"]

2. Per-tenant tool allowlist via the policy gate

Each binding gets a CEL predicate that references the principal's tenant claim. Rules live in governance.policy.tool_access.rules[]:

yaml
governance:
  policy:
    tool_access:
      default_minimum_trust: verified
      rules:
        - tool_name: "acme.*"
          minimum_trust: verified
          cel_allow_if: 'identity.attributes.tenant == "acme" || "platform" in identity.groups'
        - tool_name: "partner.*"
          minimum_trust: verified
          cel_allow_if: 'identity.attributes.tenant == "partner"'
        # shared.* falls through to default_minimum_trust (any verified caller).

A deny is observable through the standard mcpg.policy.tool_call.denied audit shape. You can also gate a single binding inline via its own governance.allow_if:

yaml
mcp:
  capabilities:
    tools:
      - name: api.admin.tenant.delete
        description: Tenant deletion  restricted to tenant_admins.
        governance:
          minimum_trust: verified
          allow_if: '"tenant_admins" in identity.groups'
        backend:
          kind: http
          url: "https://internal/api/tenants/${arguments.tenant_id}/delete"
          method: post
          expected_status_codes: [200, 204]

3. Per-tenant rate limit + quota via the rate-limit plugin

Rate limiting is plugin-only. The plugin keys its bucket on tenant identity from the same identity surface:

yaml
plugins:
  - id: dev.mcpg.rate-limit
    class: tool_gate
    source:
      oci: "ghcr.io/mcpg-dev/source-code/plugins/rate-limit:1.0.0"
    config:
      # CEL key — same expression evaluates per request, one bucket per tenant.
      key_cel: '${identity.subject_id ?? "anonymous"}'
      requests_per_minute: 600
      burst: 50

The gateway-resident gateway.server.max_sessions_per_tenant knob is the one tenant-aware quota enforced inside the session store — a per-tenant cap on concurrent sessions, independent of which CEL gate let the request through.

Hard isolation on Kubernetes — the MCPGTenant CRD

When tenants are different teams sharing a cluster, the operator's MCPGTenant CRD draws a cluster-level boundary: which namespaces a tenant owns, which plugins those namespaces may use, and hard quotas. It is cluster-scoped (only a cluster-admin defines tenants) and opt-in.

yaml
apiVersion: mcpg.dev/v1alpha1
kind: MCPGTenant
metadata:
  name: team-payments
spec:
  # Existing namespaces this tenant owns (the operator does not create them).
  namespaces:
    - payments
    - payments-staging
  # Plugin allowlist. Empty = deny-all; {name: "*"} = any cluster plugin.
  allowedPlugins:
    - name: dev.mcpg.policy.cedar              # by capability id
    - registryPrefix: ghcr.io/mcpg-dev/source-code/plugins/ # by image prefix
  # Hard quotas — count caps via a generated ResourceQuota per namespace;
  # the replica cap is enforced at admission.
  quotas:
    maxGateways: 5
    maxPluginSets: 10
    maxRoutes: 50
    maxReplicasPerGateway: 20
  # Optional: stamp a consistent tenant predicate into this tenant's
  # gateway tool_access rules so its gateways + routes share one boundary.
  identityAttribute:
    key: tenant
    value: team-payments

For each owned namespace the operator labels it mcpg.dev/tenant=<name>, binds Secret-write RBAC, and generates a ResourceQuota. The plugin allowlist and per-gateway replica cap are enforced synchronously by the admission webhook.

Soft multi-tenancy — the MCPGRoute CRD

To route multiple tenants into one shared gateway with per-tenant tool-access fan-in, use the MCPGRoute CRD. Each route carries a tenant-scoped slice of the tool catalog into the shared gateway. See the Kubernetes install guide for the operator setup.

Separate gateways per tenant

For air-gap / sovereign deployments, run one small gateway per tenant rather than one big multi-tenant deployment. Each gateway is a plain production-single-redis config scoped to that tenant's tools. The operator manages both topologies — pick per-tenant gateways when the blast-radius isolation matters more than density.