| Crates.io | mcpd-plugins-sdk |
| lib.rs | mcpd-plugins-sdk |
| version | 0.0.1 |
| created_at | 2025-11-21 15:06:06.637492+00 |
| updated_at | 2025-11-21 15:06:06.637492+00 |
| description | Rust SDK for building mcpd plugins |
| homepage | |
| repository | https://github.com/mozilla-ai/mcpd-plugins-sdk-rust |
| max_upload_size | |
| id | 1943699 |
| size | 158,203 |
Rust SDK for building mcpd plugins.
This SDK provides a simple, trait-based API for creating gRPC plugins that intercept and transform HTTP requests and responses in the mcpd middleware pipeline.
Plugin trait with only the methods you needserve() function handles all boilerplateAdd this to your Cargo.toml:
[dependencies]
mcpd-plugins-sdk = "0.0" # any 0.0.x
tokio = { version = "1", features = ["full"] }
tonic = "0.12"
Create a simple plugin that adds a custom header:
use mcpd_plugins_sdk::{
Plugin, serve, Metadata, Capabilities, HttpRequest, HttpResponse,
FLOW_REQUEST,
};
use tonic::{Request, Response, Status};
struct MyPlugin;
#[tonic::async_trait]
impl Plugin for MyPlugin {
async fn get_metadata(
&self,
_request: Request<()>,
) -> Result<Response<Metadata>, Status> {
Ok(Response::new(Metadata {
name: "my-plugin".to_string(),
version: "1.0.0".to_string(),
description: "My custom plugin".to_string(),
..Default::default()
}))
}
async fn get_capabilities(
&self,
_request: Request<()>,
) -> Result<Response<Capabilities>, Status> {
Ok(Response::new(Capabilities {
flows: vec![FLOW_REQUEST as i32],
}))
}
async fn handle_request(
&self,
request: Request<HttpRequest>,
) -> Result<Response<HttpResponse>, Status> {
let mut req = request.into_inner();
// Add custom header.
req.headers.insert("X-My-Plugin".to_string(), "processed".to_string());
Ok(Response::new(HttpResponse {
r#continue: true,
modified_request: Some(req),
..Default::default()
}))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
serve(MyPlugin, None).await?;
Ok(())
}
Important: When deploying plugins to run with mcpd (especially in containers), you should build static, self-contained binaries that don't depend on system libraries. The mcpd container won't have Rust runtime libraries available.
For production use with mcpd, build a fully static binary:
# Install musl target (one-time setup).
rustup target add x86_64-unknown-linux-musl
# Build static binary.
cargo build --release --target x86_64-unknown-linux-musl
# The resulting binary is completely self-contained and ready for mcpd.
./target/x86_64-unknown-linux-musl/release/my-plugin --address /tmp/my-plugin.sock
For local development and testing:
# Build the plugin.
cargo build --release
# Run with Unix socket (Linux/macOS).
./target/release/my-plugin --address /tmp/my-plugin.sock --network unix
# Run with TCP (any platform).
./target/release/my-plugin --address localhost:50051 --network tcp
The Plugin trait defines the interface for all plugins. All methods have default implementations, so you only need to override what you need:
#[tonic::async_trait]
pub trait Plugin: Send + Sync + 'static {
// Identity methods.
async fn get_metadata(&self, _request: Request<()>) -> Result<Response<Metadata>, Status>;
async fn get_capabilities(&self, _request: Request<()>) -> Result<Response<Capabilities>, Status>;
// Lifecycle methods.
async fn configure(&self, _request: Request<PluginConfig>) -> Result<Response<()>, Status>;
async fn stop(&self, _request: Request<()>) -> Result<Response<()>, Status>;
// Health checks.
async fn check_health(&self, _request: Request<()>) -> Result<Response<()>, Status>;
async fn check_ready(&self, _request: Request<()>) -> Result<Response<()>, Status>;
// Request/response handling.
async fn handle_request(&self, request: Request<HttpRequest>) -> Result<Response<HttpResponse>, Status>;
async fn handle_response(&self, response: Request<HttpResponse>) -> Result<Response<HttpResponse>, Status>;
}
Plugins can participate in two processing flows:
FLOW_REQUEST: Process incoming HTTP requests before they reach the upstream serverFLOW_RESPONSE: Process outgoing HTTP responses before they return to the clientDeclare which flows your plugin supports in the get_capabilities() method:
async fn get_capabilities(&self, _request: Request<()>) -> Result<Response<Capabilities>, Status> {
Ok(Response::new(Capabilities {
flows: vec![FLOW_REQUEST as i32, FLOW_RESPONSE as i32], // Support both flows.
}))
}
The handle_request() method can respond in three ways:
async fn handle_request(&self, request: Request<HttpRequest>) -> Result<Response<HttpResponse>, Status> {
Ok(Response::new(HttpResponse {
r#continue: true,
..Default::default()
}))
}
async fn handle_request(&self, request: Request<HttpRequest>) -> Result<Response<HttpResponse>, Status> {
let mut req = request.into_inner();
// Modify the request.
req.headers.insert("X-Custom".to_string(), "value".to_string());
Ok(Response::new(HttpResponse {
r#continue: true,
modified_request: Some(req),
..Default::default()
}))
}
async fn handle_request(&self, request: Request<HttpRequest>) -> Result<Response<HttpResponse>, Status> {
// Return error response directly.
Ok(Response::new(HttpResponse {
r#continue: false,
status_code: 401,
body: b"Unauthorized".to_vec(),
..Default::default()
}))
}
The SDK includes three complete example plugins:
Adds custom headers to all requests.
cargo run --example simple_plugin -- --address /tmp/simple.sock
Validates Bearer tokens and returns 401 for unauthorized requests.
cargo run --example auth_plugin -- --address /tmp/auth.sock
Implements token bucket rate limiting per client IP address.
cargo run --example rate_limit_plugin -- --address /tmp/ratelimit.sock
mcpd runs plugins as separate processes and may run in containerized environments that don't have Rust runtime libraries. Always use static binaries for production deployments.
# Install musl target (one-time setup).
rustup target add x86_64-unknown-linux-musl
# Build static binary.
cargo build --release --target x86_64-unknown-linux-musl
# The resulting binary is completely self-contained.
ls -lh target/x86_64-unknown-linux-musl/release/my-plugin
Use cross for easy cross-compilation to different platforms:
# Install cross (one-time setup).
cargo install cross
# Build for different platforms.
cross build --release --target x86_64-unknown-linux-musl # Linux x86_64 (static)
cross build --release --target aarch64-unknown-linux-musl # Linux ARM64 (static)
cross build --release --target x86_64-apple-darwin # macOS x86_64
cross build --release --target aarch64-apple-darwin # macOS ARM64 (Apple Silicon)
Add this to your plugin's Cargo.toml for smaller binaries (typically 3-5 MB):
[profile.release]
opt-level = "z" # Optimize for size.
lto = true # Enable link-time optimization.
codegen-units = 1 # Better optimization, slower compile.
strip = true # Strip symbols.
panic = "abort" # Smaller panic handler.
--target x86_64-unknown-linux-musl for static linkingldd my-plugin (should show "not a dynamic executable")--helpPlugins receive configuration via the configure() method:
async fn configure(&self, request: Request<PluginConfig>) -> Result<Response<()>, Status> {
let config = request.into_inner();
// Access custom configuration.
if let Some(value) = config.custom_config.get("my_setting") {
// Use configuration value.
}
// Access telemetry configuration.
if let Some(telemetry) = config.telemetry {
// Setup OpenTelemetry with provided settings.
}
Ok(Response::new(()))
}
Configuration is provided by mcpd from YAML files:
plugins:
my-plugin:
custom_config:
my_setting: "value"
max_requests: "100"
The SDK provides a PluginError type for error handling:
use mcpd_plugins_sdk::PluginError;
fn validate_config(value: &str) -> Result<u32, PluginError> {
value.parse().map_err(|_| {
PluginError::Configuration(format!("Invalid number: {}", value))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_handle_request() {
let plugin = MyPlugin::new();
let request = Request::new(HttpRequest {
method: "GET".to_string(),
path: "/test".to_string(),
..Default::default()
});
let response = plugin.handle_request(request).await.unwrap();
assert_eq!(response.into_inner().r#continue, true);
}
}
See the examples directory for complete integration test patterns.
The SDK automatically downloads and compiles protocol buffers from the mcpd-proto repository during the build process.
To use a specific proto version:
PROTO_VERSION=v0.0.3 cargo build
This crate requires Rust 1.83 or later. We follow a conservative MSRV policy and will clearly communicate any MSRV bumps.
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Licensed under the Apache License, Version 2.0. See LICENSE for details.