MCPG
Concepts
Concepts

Governance model

Every tool call flows through one lifecycle — who is calling (access), are they allowed (policy), does a human need to sign off (approvals), and what gets recorded (audit). One umbrella, fail-closed by default.

MCPG governs tool calls with a single, coherent lifecycle:

access → policy → approvals → audit

Who is calling? → Are they allowed? → Does a human need to sign off? → What gets recorded?

These four blocks share one config umbrella, governance:, precisely so the story reads as one chain rather than four scattered top-level peers. (A fifth block, governance.quotas, registers rate-limit / budget / concurrency policies that bindings opt into.) This page is the mental model; the exact keys live in the configuration reference.

yaml
governance:
  access:    {}   # who is calling — establish identity
  policy:    {}   # are they allowed — trust floor + CEL
  approvals: {}   # does a human need to sign off
  audit:     {}   # what gets recorded — compliance evidence
  quotas:    {}   # within what limits

Every child defaults to its own zero-value, so an empty governance: block is valid YAML — it produces anonymous identity, an untrusted-by-default policy, no human-gate signing key, and the default local-file audit sink. You opt into strictness; you don't opt out of it.

1. Access — who is calling

Access establishes inbound identity before anything else runs. The gateway resolves the caller through a priority chain and stamps the request with a trust level:

Trust levelIdentitySource
UnauthenticatedAnonymousno identity present
HeaderAssertedHTTP headerx-mcpg-subject-id (an upstream proxy vouches for it)
VerifiedVerifieda JWT verified via JWKS or OIDC/OAuth

Configuration lives under governance.access. The two verification modes — jwks (single-provider JWT) and oidc_oauth (multi-provider OIDC discovery + introspection) — are mutually exclusive. Richer or chained identity (mTLS, SPIFFE, API keys, custom resolvers) loads as identity-provider plugins.

yaml
governance:
  access:
    oidc_oauth:
      providers:
        - issuer: "https://example.okta.com"
          audiences: ["mcpg-gateway"]
          verification:
            kind: oidc_jwks          # verify RS256 JWTs against the issuer's JWKS
            allowed_algs: ["RS256"]

When access is unset, the gateway accepts unauthenticated callers and stamps every request identity_kind: anonymous — so the policy gate can deny them. The gateway fails closed: identity-verification failures are denials, not fallbacks to anonymous. Full setup is in the Identity guide.

2. Policy — are they allowed

Policy is the authorization gate every tool call crosses before dispatch. It has two layers, evaluated in order:

  1. Trust-level floor. The caller's trust level is compared against the tool's minimum_trust requirement. A binding that requires verified is never reachable by an anonymous caller.
  2. CEL expressions. A global cel_allow_if and per-tool cel_allow_if expressions evaluate over a context of tool_name, trust_level, principal_id, auth_provider, and identity_kind.
yaml
governance:
  policy:
    tool_access:
      default_minimum_trust: verified
      cel_allow_if: 'trust_level == "verified"'

The trust floor is set both globally (governance.policy.tool_access) and per-binding (mcp.capabilities.tools[].governance.minimum_trust / cel_allow_if), so authorization is a property of each binding, not a separate table you have to keep in sync.

For richer decisions, point governance.policy.engine at a policy-engine plugin — Cedar, OPA, or Casbin — and chain several for defense-in-depth. The gateway's built-in trust gate still runs first as a floor; the plugin engines layer on top. The outcome is binary: Allow proceeds; Deny returns a structured JSON-RPC error carrying an audit reason. See the Policy guide.

3. Approvals — does a human need to sign off

Some tool calls shouldn't run on policy alone. The approvals stage lets a tool-gate return PendingApproval instead of Allow/Deny: the call suspends, a human-approval request is delivered (Slack, email, PagerDuty), and the call resumes only on a signed approval within a grace window.

yaml
governance:
  approvals:
    signing_key_env: MCPG_APPROVAL_SIGNING_KEY
    callback_base_url: "https://mcpg.example.com"

The signing key authenticates the approval callback so a forged "approved" response can't release a held call. When unset, the runtime falls back to a random per-process key — fine for dev and tests, not for production, where a stable signing key is required so approvals survive a restart.

4. Audit — what gets recorded

Audit is the evidence layer. It lives under governance: (not observability:) on purpose — the audit trail is evidence of governance, so it reads alongside access, policy, and approvals rather than next to metrics.

Audit fans out to one or more sinks; each sink's kind: is a plugin id resolved against the registered audit-sink plugins at boot:

yaml
governance:
  audit:
    enabled: true
    required: true            # refuse to start if no sink is serving
    on_failure: fail_closed   # no action without a durable record
    sinks:
      - kind: dev.mcpg.builtin.audit.local-file
        config: { path: "/var/log/mcpg/audit.log" }

Two settings carry the compliance posture:

  • required: true (default) — the gateway refuses to start unless at least one audit sink is serving traffic after plugin registration. You cannot accidentally run production with no audit trail.
  • on_failure: fail_closed (default) — captures the operator's intent that no action proceed without a durable record. Today the boot-time required guarantee is the enforced half (the gateway won't start without a serving sink); per-emit request refusal on a failed write is the configured posture and the runtime hookup is landing in a follow-up — failures are currently metriced. The alternative, fail_open, is the explicit dev/CI signal.

The default sink is the built-in dev.mcpg.builtin.audit.local-file, which writes hash-chained JSON Lines you point a SIEM forwarder at. The audit channel is intentionally orthogonal to observability.enabled — you can disable observability for a debugging run, but compliance audit stays on. Wiring detail is in the Observability guide.

Why one umbrella

Pulling access, policy, approvals, and audit under a single governance: block is the whole point: the four steps are one lifecycle. A call is admitted by access, authorized by policy, optionally gated by approvals, and recorded by audit — and the defaults make the secure posture the easy one. Identity failures deny, an undeclared trust floor denies, a broken audit sink refuses the call, and the gateway won't even boot without a working audit path.

Where to go next