Plugin authoring
Write a plugin in Rust (cdylib) or as a WASM Component. Sign it, pack it, distribute it via OCI, and wire it into a gateway config.
MCPG plugins are how the gateway gets new capabilities — identity backends, policy engines, transforms, audit sinks, backends. Two loaders ship: native Rust cdylibs (max performance, full ABI) and WASM Components (sandboxed, language-agnostic).
This guide walks through writing a transform plugin in native Rust — small enough to fit on one page, useful enough to teach the SDK.
What's a transform plugin?
A transform plugin reshapes a tool call's arguments (transform_arguments) and/or its
result (transform_result). Common use cases:
- Mask PII fields in the response
- Normalize timestamps to UTC
- Strip unsupported fields for older upstream tools
Transform plugins (class: transform) are wired into a pipeline backend's steps[] via a
plugin_transform step — see the wiring section below.
1. Scaffold the crate
mcpg-plugin new --kind transform --name redact-emails
This generates a crate with the right Cargo.toml and a plugin.yaml descriptor. The
manifest:
# plugin.yaml
schema: mcpg.dev/plugin/v1
id: dev.example.transform.redact-emails
name: Redact Emails
description: Replace email addresses in tool results with [redacted].
class: transform
runtime: native-cdylib-v1
protocol_version: "1.0"
required_capabilities: []
The crate's Cargo.toml declares a cdylib against the SDK:
[lib]
crate-type = ["cdylib"]
[dependencies]
mcpg-plugin-sdk = "1"
mcpg-plugin-protocol = "1"
serde_json = "1"
regex = "1"
2. Implement the trait
// src/lib.rs — a sync transform (the common case; no host calls, pure compute).
use mcpg_plugin_protocol::{PluginContext, PluginManifest, TransformResult, firstparty_manifest};
use mcpg_plugin_sdk::ffi::SyncTransform;
use regex::Regex;
use serde_json::Value;
pub struct RedactEmails {
manifest: PluginManifest,
pattern: Regex,
}
impl RedactEmails {
pub fn new(_config_json: &str) -> Self {
Self {
// First-party plugins build the manifest with `firstparty_manifest!`;
// a third-party plugin constructs a `PluginManifest` from its
// `plugin.yaml` descriptor instead.
manifest: firstparty_manifest! {
id: "dev.example.transform.redact-emails",
name: "Redact Emails",
class: Transform,
},
pattern: Regex::new(r"[\w.-]+@[\w.-]+\.[a-z]+").unwrap(),
}
}
}
impl SyncTransform for RedactEmails {
fn manifest(&self) -> &PluginManifest {
&self.manifest
}
fn transform_arguments(
&self,
_ctx: &PluginContext,
_arguments: &Value,
_config: &Value,
) -> TransformResult {
// We only redact results; pass request arguments through untouched.
TransformResult::Unchanged
}
fn transform_result(
&self,
_ctx: &PluginContext,
result: &Value,
_config: &Value,
) -> TransformResult {
let redacted = self.pattern.replace_all(&result.to_string(), "[redacted]").to_string();
match serde_json::from_str(&redacted) {
Ok(value) => TransformResult::Modified { value },
Err(message) => TransformResult::Error { message: message.to_string() },
}
}
}
// cdylib export — feature-gated so a plain workspace build emits only the rlib.
#[cfg(feature = "cdylib-export")]
mcpg_plugin_sdk::declare_plugin! {
plugin_id: "dev.example.transform.redact-emails",
plugin_version: env!("CARGO_PKG_VERSION"),
descriptor_yaml: include_str!("../plugin.yaml"),
capabilities: &[],
entities: [
transform as redact_emails {
inner_name: "",
plugin_type: RedactEmails,
factory: |cfg: &str, _host: ::mcpg_plugin_sdk::HostHandle| RedactEmails::new(cfg),
},
],
}
3. Build
cargo build --release
# Output: target/release/libredact_emails.so (Linux)
This build targets your host; release plugin cdylibs are published as per-platform OCI artifacts across x86_64 and aarch64 Linux (glibc and musl), Apple Silicon, and x86_64 Windows.
4. Sign
Plugin artifacts must be Ed25519-signed before a gateway with signature verification enabled will load them.
# Generate a key (the CLI does not do keygen — use any Ed25519 tool):
ssh-keygen -t ed25519 -f signing-key
# Sign the artifact (writes target/release/libredact_emails.so.sig):
mcpg-plugin sign --key signing-key target/release/libredact_emails.so
For KMS / HSM signing, use mcpg-plugin sign --subprocess <cmd> — the key never leaves the
KMS.
5. Pack
Bundle the descriptor, artifact, and signature into a distributable zip:
mcpg-plugin pack \
--descriptor plugin.yaml \
--artifact target/release/libredact_emails.so \
--version 0.1.0 \
--signature target/release/libredact_emails.so.sig \
--output redact-emails-0.1.0.zip
6. Push to OCI
mcpg-plugin push redact-emails-0.1.0.zip \
ghcr.io/example/mcpg-plugins/redact-emails:0.1.0
OCI 1.1 registries that work: GHCR, ECR, GCR, Harbor, Zot, Quay, Artifactory 7.6+.
7. Wire it into a gateway config
Load the plugin under plugins[], pinning the signature to your trusted key, then invoke
it from a pipeline backend's steps[]:
plugins:
- id: dev.example.transform.redact-emails
class: transform
source:
oci: "ghcr.io/example/mcpg-plugins/redact-emails:0.1.0"
signature:
trusted_keys:
- id: example-publisher
pem: |
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...
-----END PUBLIC KEY-----
mcp:
capabilities:
tools:
- name: support.lookup
description: Look up a support ticket, redacting emails from the result.
backend:
kind: pipeline
steps:
- id: fetch
kind: http
url: "https://support.internal/api/tickets/${arguments.id}"
method: get
- id: redact
kind: plugin_transform
plugin: dev.example.transform.redact-emails
The gateway pulls the OCI artifact, verifies its signature against your trusted key, loads
it, and runs the redact step after fetch. Validate the whole config with
mcpg config check config.yaml before booting.
WASM Component plugins
WASM Components compile from any language with a Component Model toolchain. The protocol is
the same; only the ABI differs — build with cargo component build --release --target wasm32-wasip2 and pack infers the wasi-v1 runtime from the .wasm extension.
The trade-off: WASM gives sandboxing, faster cold-start, and language-agnosticism. Native Rust gives substantially more throughput on hot paths. Use WASM for untrusted plugins or polyglot teams; native when you control the supply chain and need performance.
Capabilities
Plugins declare what host capabilities they need (network_outbound,
filesystem_read, etc.) in required_capabilities. The gateway refuses to
start if a plugin requires a capability the operator hasn't explicitly granted via the
entry's granted_capabilities:. See the security model for the
full capability set.
What's next
- Plugins and the plugin protocol — how the loader, ABI, and signing chain fit together
- Plugin catalogue — first-party backends, identity, policy, transform, and observability plugins to crib from. All Apache-2.0.