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:
| Kind | Composition | What it extends |
|---|---|---|
tool_gate | chain | Per-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. |
transform | chain | Mutate tool arguments (pre) and results (post) — PII masking, schema migration. |
identity_provider | chain (first-match) | Resolve caller identity from request headers and metadata. |
backend | keyed by kind() | Dispatch a tool call to an upstream (HTTP, gRPC, GraphQL, SQL, Kafka, NATS, command, mock, LLM providers). The terminal step. |
policy_engine | keyed by name | Centralized authorization (OPA / Cedar / Casbin) returning a structured decision with obligations and redactions. |
audit_sink | fan-out | Durable, tamper-evident audit delivery — must persist before acknowledging. |
credential_issuer | keyed | Per-request backend credential issuance with leases. |
approval_notifier | keyed/fan-out | Deliver human-approval requests (Slack, email, PagerDuty). |
cluster | singleton | Cross-node coordination: leader election, locks/leases, pub/sub, KV. |
secret_provider | keyed by scheme | URI-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
backendis keyed by itskind()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:
| Runtime | Loading | Use |
|---|---|---|
| Native cdylib | dlopen + vtable calls; SHA-256 + Ed25519 verified | High-performance first-party and trusted third-party. The bulk of the catalog. |
| Static first-party | Compiled into the gateway binary, same registration path, no FFI | The built-in dev.mcpg.builtin.* zero-dependency defaults (memory store/cache, env/file secrets, single-node cluster, stderr logs, local-file audit). |
| WASI component | Wasmtime, capability-confined sandbox | Language-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
- Plugin authoring — write, sign, pack, and wire a plugin in Rust or WASM.
- Architecture — where plugins sit in the request flow.
- Governance model — the gates plugins implement.
- The MCPG security model — how signing, capabilities, and policy compose.