MCPG
Concepts
Concepts

Plugins and the plugin protocol

MCPG is a thin host; everything with a policy, an upstream, or a side-effect is a plugin loaded across a stable binary boundary. The entity model, the three runtimes, capabilities, signing, and trust — explained.

The thesis of MCPG's plugin protocol is one sentence:

The gateway core is a thin host. Everything with a policy, an upstream, or a side-effect is a plugin loaded across a stable binary boundary.

There are no statically-required backends, no built-in authorization engine that can't be swapped, no hard-coded audit format. The core provides a plugin framework — loading, validation, capability enforcement, lifecycle — and a dispatch pipeline with ordered seams where plugins run. This page is the mental model; if you want to write one, jump to Plugin authoring.

Why an in-process binary ABI

A plugin runs in-process, loaded as a native shared library (cdylib) and called through a C-stable function-pointer table (a "vtable"). That is a deliberate trade against running plugins as separate processes:

  • Performance. A single tool call may cross an identity resolver, a transform chain, a tool-gate chain, a backend, and a fan-out of audit/metrics sinks. In-process vtable dispatch is a function-pointer call plus a JSON (de)serialization — not an IPC round-trip, a socket, or a process spawn per call.
  • Isolation where it counts. A Rust extern "C" boundary must never unwind, so every slot is wrapped in a panic guard that converts a plugin panic into a well-defined fail-closed value (deny, error, null handle) rather than aborting the gateway.
  • Stability through versioning. The ABI carries a version sentinel; a plugin built against a different binary layout is refused at load.

For untrusted or language-agnostic code, a sandboxed WASI runtime exists alongside the native path — same registry, same host-facing traits, but capability-confined under Wasmtime.

Plugins vs entities

The unit of extension is the entity, not the plugin:

  • A plugin is a package — one shared library with one reverse-DNS manifest id (e.g. dev.mcpg.policy.cedar).
  • An entity is a single typed extension it provides.

One plugin may provide many entities, of one kind or several. For example, backend/llms/openai registers many backend entities (openai_chat, openai_embedding, openai_image, and their azure_openai_* twins) from one library; a Slack-approval plugin registers a tool_gate, an approval_notifier, and an http_route (to receive Slack's callback) from one library. This is why the catalog has far more capabilities than it has packages.

The 21 entity kinds

Every entity has a kind, and each kind has its own vtable and its own composition rule. There are 21 kinds. A representative slice:

KindCompositionWhat it extends
tool_gatechainPer-call admission: allow / deny / challenge / pending-approval, pre- and post-dispatch. Home of policy, rate-limit, circuit-breaker, response-cache, payment, IP-allowlist, audit-trigger, and human-approval logic.
transformchainMutate tool arguments (pre) and results (post) — PII masking, schema migration.
identity_providerchain (first-match)Resolve caller identity from request headers and metadata.
backendkeyed by kind()Dispatch a tool call to an upstream (HTTP, gRPC, GraphQL, SQL, Kafka, NATS, command, mock, LLM providers). The terminal step.
policy_enginekeyed by nameCentralized authorization (OPA / Cedar / Casbin) returning a structured decision with obligations and redactions.
audit_sinkfan-outDurable, tamper-evident audit delivery — must persist before acknowledging.
credential_issuerkeyedPer-request backend credential issuance with leases.
approval_notifierkeyed/fan-outDeliver human-approval requests (Slack, email, PagerDuty).
clustersingletonCross-node coordination: leader election, locks/leases, pub/sub, KV.
secret_providerkeyed by schemeURI-addressable secret resolution (vault://, env://, file://).

The remaining kinds cover watch strategies, HTTP routes, log / telemetry / metrics sinks, stores, caches, config providers, transports, catalog providers, and content stores. The full table is in the configuration reference.

Four composition models

How the host combines multiple entities of a kind is intrinsic to the kind — it isn't configurable:

  • Chain — ordered list, run in declaration order; the first terminal decision wins (tool-gates, transforms, identity, catalog).
  • Keyed — exactly one active entity per key; the host routes by key (a backend is keyed by its kind() string, a secret provider by URI scheme).
  • Fan-out — every registered entity receives every event; no entity can suppress another (the four sink kinds: audit, log, telemetry, metrics).
  • Singleton — exactly one active gateway-wide (cluster).

Composition is why a tool-gate chain can short-circuit on the first deny while an audit fan-out guarantees every sink sees every event.

The three runtimes

A plugin's declared runtime: selects how it is loaded — but all three land on the same registry and the same host-facing traits:

RuntimeLoadingUse
Native cdylibdlopen + vtable calls; SHA-256 + Ed25519 verifiedHigh-performance first-party and trusted third-party. The bulk of the catalog.
Static first-partyCompiled into the gateway binary, same registration path, no FFIThe built-in dev.mcpg.builtin.* zero-dependency defaults (memory store/cache, env/file secrets, single-node cluster, stderr logs, local-file audit).
WASI componentWasmtime, capability-confined sandboxLanguage-agnostic, untrusted/third-party plugins (e.g. masking transforms).

The critical point: a built-in store and a cdylib store are indistinguishable to the dispatch pipeline. Built-ins are not privileged core code — they are first-party plugins that happen to be statically linked through the same path as everything else.

Bidirectional: plugins call back into the host

Plugins are not one-way. Through a host-services table they call back into the gateway — to resolve secrets and credentials, read config, emit audit and metrics, store and fetch content, and even re-enter the dispatch pipeline to invoke another tool (the primitive behind agentic tool-calling). The host serves these synchronous FFI callbacks by re-entering its async runtime.

The trust model: capabilities, signing, sandbox

A plugin is powerful — it runs in-process. Three layers keep that safe.

Capabilities (least privilege, enforced at boot)

A plugin declares the host capabilities it needs; the operator grants them per entry; boot fails closed on anything ungranted or unknown. The capability set is explicit — network_outbound, audit_write, metric_emit, cluster primitives, http_route_serve, and arg-bearing ones like filesystem_read { paths }, secrets_read { schemes }, and credential_issue { kinds }.

Boot validation has exactly one passing state, Satisfied. Anything else — the plugin requires a capability the operator didn't grant, the operator granted one the plugin doesn't recognize, or the plugin needs a capability this host version doesn't know — fails the load with a message naming the plugin. A plugin can never quietly acquire more reach than its operator approved.

Signing and revocation (provenance)

Native artifacts are verified with Ed25519 over the raw bytes against operator-configured trusted keys. Policy is per-registry:

  • Disabled — skip (emits an audit event).
  • Warn (the default) — attempt, log on failure, load anyway.
  • Enforce — refuse on missing, invalid, or unverifiable signature.

Load order is: optional SHA-256 content pin (hard-fail on mismatch, any policy) → signature per policy → revocation-list lookup by hash (a validly-signed but revoked artifact is refused with a distinct error). Operators must set Enforce in production — the Warn default will load unsigned native plugins, which is right for development and wrong for production.

The defence stack

Capabilities and signing sit inside a wider set of guards:

  • Panic isolation — a plugin panic is a fail-closed value, never a gateway abort.
  • SSRF / DNS-rebinding guard — the shared HTTP client refuses private and loopback targets and pins the validated address so a DNS flip can't rebind to an internal host.
  • FFI limits — per-slot timeouts and a payload-size cap bound a slow or oversized plugin.
  • WASI sandbox — capability-confined execution with fuel and memory limits for the untrusted runtime.

Distribution

Plugins are independently versioned, signed, and distributable via OCI registries — the same place container images live. In Kubernetes the operator manages them declaratively through the MCPGPlugin, MCPGPluginSet, MCPGRevocationList, and MCPGPluginMirror CRDs (the mirror serves air-gapped installs). See Kubernetes install.

Where to go next