View on GitHub →
← Back to Home

Secure Your Rust MCP Server

An interactive guide to implementing SchemaPin with Axum.

The Challenge: Insecure Tool Schemas

The Model Context Protocol (MCP) relies on JSON schemas to define tool capabilities. If a schema is tampered with, an attacker can hijack your AI agent's tools. This is a critical supply chain vulnerability.

SchemaPin solves this by using cryptographic signatures to guarantee that tool schemas are authentic and unmodified.

Advanced Demo: Open In Colab

Step 1: Install CLI Tools

Install the SchemaPin CLI tools via Cargo. These provide key generation and schema signing.

cargo install schemapin

Step 2: Create & Sign Your Schema

Create a file named get_issue_schema.json with the tool's definition.

{
  "name": "get_issue",
  "description": "Gets the contents of an issue within a repository",
  "input_schema": {
    "type": "object",
    "properties": {
      "owner": { "type": "string", "description": "Repository owner" },
      "repo": { "type": "string", "description": "Repository name" },
      "issue_number": { "type": "number", "description": "Issue number" }
    },
    "required": ["owner", "repo", "issue_number"]
  }
}

Generate your cryptographic key pair.

schemapin-keygen

Now, use your private key to sign the schema, producing a secure, signed version.

schemapin-sign --schema get_issue_schema.json \
  --key private.key \
  --output get_issue_signed.json

Step 3: Host Public Key for Discovery

For services to verify your signatures, they must be able to find your public key. The standard approach is to host it at a .well-known/schemapin.json URL on your domain. Use the automated well-known server to host your public key.

Run the well-known server, which automatically creates the proper directory structure and serves your public key. Keep this server running in a separate terminal.

python -m schemapin.well_known_server --public-key-file public.key --port 8000

Step 4: Build the Secure Rust Server

Setup

cargo init secure-server
cd secure-server
cargo add schemapin axum tokio --features tokio/full
touch src/main.rs

Add to src/main.rs

use axum::{routing::get, Json, Router};
use schemapin::{core, discovery};
use std::collections::HashMap;
use std::sync::Arc;

type ToolSchemas = Arc<HashMap<String, serde_json::Value>>;

async fn load_and_verify_schema(
    path: &str,
    host: &str,
    key_id: &str,
) -> Option<serde_json::Value> {
    println!("Verifying {path}...");
    let signed_bytes = std::fs::read(path).ok()?;
    println!("Discovering key from {host}...");
    let public_key = discovery::discover_public_key(host, key_id)
        .await
        .ok()?;
    match core::verify(&signed_bytes, &public_key) {
        Ok(verified_bytes) => {
            let schema: serde_json::Value =
                serde_json::from_slice(&verified_bytes).ok()?;
            let name = schema["name"].as_str()?.to_string();
            println!("SUCCESS: Verified '{name}'.");
            Some(schema)
        }
        Err(e) => {
            eprintln!("ERROR: Verification failed! {e}");
            None
        }
    }
}

async fn get_tools(tools: axum::extract::State<ToolSchemas>) -> Json<Vec<String>> {
    Json(tools.keys().cloned().collect())
}

#[tokio::main]
async fn main() {
    let verified = load_and_verify_schema(
        "get_issue_signed.json",
        "http://localhost:8000",
        "default",
    )
    .await;

    match verified {
        Some(schema) => {
            let mut map = HashMap::new();
            let name = schema["name"].as_str().unwrap().to_string();
            map.insert(name, schema);
            let tools: ToolSchemas = Arc::new(map);

            let app = Router::new()
                .route("/tools", get(get_tools))
                .with_state(tools);

            let listener =
                tokio::net::TcpListener::bind("127.0.0.1:5004").await.unwrap();
            println!("Rust MCP server on port 5004");
            axum::serve(listener, app).await.unwrap();
        }
        None => {
            eprintln!("\nShutting down due to verification failure.");
            std::process::exit(1);
        }
    }
}

Run and Test Security

Run cargo run. Then, stop the server, tamper with the signature in get_issue_signed.json, and run it again to see the failure.

Valid Schema Tampered
Verifying get_issue_signed.json...
Discovering key from http://localhost:8000...
SUCCESS: Verified 'get_issue'.
Rust MCP server on port 5004