Plugins
Plugins9 min

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

bash
mcpg-plugin new --kind transform --name redact-emails

This generates a crate with the right Cargo.toml and a plugin.yaml descriptor. The manifest:

yaml
# 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:

toml
[lib]
crate-type = ["cdylib"]

[dependencies]
mcpg-plugin-sdk = "1"
mcpg-plugin-protocol = "1"
serde_json = "1"
regex = "1"

2. Implement the trait

rust
// 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

bash
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.

bash
# 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:

bash
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

bash
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[]:

yaml
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