BetaMCPG is in public beta. Join the waitlist for managed cloud + early-access features.
MCPG
beta
All guides
Extending9 min

Plugin authoring

Write a plugin in Rust or as a WASM Component. Sign it. Distribute via OCI.

MCPG plugins are how the gateway gets new capabilities — identity backends, policy engines, transforms, bindings, observability sinks. Two loaders ship: native Rust cdylibs (max performance, full async-trait) and WASM Component Model (sandboxed, language-agnostic).

This guide walks through writing a transform plugin in native Rust — small enough to fit in one page, useful enough to teach the SDK.

What's a transform plugin?

A transform plugin runs before dispatch (mutating the request) and/or after dispatch (mutating the response). Common use cases:

  • Mask PII fields (transforms.masking)
  • Inject correlation headers
  • Normalize timestamps to UTC
  • Strip unsupported fields for older upstream tools

1. Set up the crate

toml
# Cargo.toml
[package]
name = "mcpg-plugin-redact-emails"
version = "0.1.0"
edition = "2024"

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

[dependencies]
mcpg-plugin-sdk = "1.0"
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
regex = "1"

2. Implement the trait

rust
// src/lib.rs
use async_trait::async_trait;
use mcpg_plugin_sdk::{
    register_plugin, transform::*, Capability, PluginInfo, ToolRequest, ToolResponse,
};
use regex::Regex;

struct RedactEmails {
    pattern: Regex,
}

#[async_trait]
impl TransformPlugin for RedactEmails {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            id: "redact-emails".into(),
            version: "0.1.0".into(),
            capabilities: vec![],
        }
    }

    async fn before_dispatch(&self, _req: &mut ToolRequest) -> Result<(), Error> {
        Ok(())
    }

    async fn after_dispatch(&self, resp: &mut ToolResponse) -> Result<(), Error> {
        if let Some(text) = resp.text_mut() {
            *text = self.pattern.replace_all(text, "[redacted]").to_string();
        }
        Ok(())
    }
}

register_plugin!(RedactEmails {
    pattern: Regex::new(r"[\w\.-]+@[\w\.-]+\.[a-z]+").unwrap(),
});

3. Build

bash
cargo build --release
# Output: target/release/libmcpg_plugin_redact_emails.so (Linux)
#         target/release/libmcpg_plugin_redact_emails.dylib (macOS)

4. Sign

Plugin artifacts must be Ed25519-signed before they can be loaded by a gateway with require_signed_plugins: true (default in production).

bash
mcpg-plugins sign \
  --key path/to/signing-key.pem \
  target/release/libmcpg_plugin_redact_emails.so

This produces a sidecar .sig file. The gateway verifies on load against the public key configured in the gateway's plugin policy.

5. Pack

Bundle the artifact, sidecar signature, and plugin descriptor into a single archive:

bash
mcpg-plugins pack \
  --descriptor plugin.json \
  --artifact target/release/libmcpg_plugin_redact_emails.so \
  --signature target/release/libmcpg_plugin_redact_emails.so.sig \
  --out redact-emails-0.1.0.zip

The descriptor:

json
{
  "id": "redact-emails",
  "version": "0.1.0",
  "kind": "transform",
  "required_capabilities": [],
  "tags": ["security", "privacy"]
}

6. Push to OCI

bash
mcpg-plugins push \
  --tag oci.example.com/mcpg-plugins/redact-emails:0.1.0 \
  redact-emails-0.1.0.zip

OCI registries that work: ghcr.io, ECR, GCR, Harbor, Zot, Quay.

7. Use it

In your gateway config:

yaml
plugins:
  - id: redact-emails
    source:
      oci: oci.example.com/mcpg-plugins/redact-emails:0.1.0
      public_key: |
        -----BEGIN PUBLIC KEY-----
        MCowBQYDK2VwAyEA...
        -----END PUBLIC KEY-----

bindings:
  - id: support-tool
    transforms:
      - redact-emails

The gateway pulls, verifies, loads, and runs.

WASM Component plugins

WASM Components compile from any language with a Component Model toolchain (Rust, Go, JavaScript, Python via componentize-py). The protocol is the same; only the ABI differs:

rust
// Same transform, but as a WASM Component
wit_bindgen::generate!("redact-emails");

struct Component;

impl Guest for Component {
    fn after_dispatch(resp: ToolResponse) -> Result<ToolResponse, String> {
        // ...
    }
}

Build with cargo component build --release --target wasm32-wasip2.

The trade-off: WASM gives sandboxing, faster cold-start, and language-agnosticism. Native Rust gives ~10× more throughput on hot paths. Use WASM when you want untrusted plugins or polyglot teams; native when you control the supply chain and need performance.

Capabilities

Plugins declare what host capabilities they need (cap.host.outbound_http, cap.host.secret_store, etc.). The gateway refuses to start if a plugin requires a capability the operator hasn't explicitly granted. See the security model article for the full capability set.

What's next

  • Plugin architecture — how the loader, ABI, and signing chain fit together
  • The OSS catalog: 38 plugins to crib from. All Apache-2.0.