Skip to content

Latest commit

 

History

History
2827 lines (2367 loc) · 98.8 KB

File metadata and controls

2827 lines (2367 loc) · 98.8 KB

Model Context Protocol (MCP) in Rust: Complete Tutorial

This tutorial provides a comprehensive guide to building MCP (Model Context Protocol) servers in Rust, progressing from simple examples to production-ready enterprise applications.

Table of Contents

  1. Introduction to MCP
  2. Getting Started
  3. Basic Examples (1-5)
  4. Intermediate Examples (6-10)
  5. Advanced Examples (11-15)
  6. Enterprise Examples (16-20)
  7. Best Practices
  8. Production Deployment

Introduction to MCP

The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to LLMs (Large Language Models). It enables AI-powered tools and workflows by solving the M×N integration problem through a unified protocol.

Key MCP Concepts

  • Tools: Functions that LLMs can call to perform actions
  • Resources: Data sources that LLMs can access (documents, databases, APIs)
  • Prompts: Reusable prompt templates
  • Sampling: LLM text generation capabilities

Why Rust for MCP?

Rust provides several advantages for MCP server development:

  • Memory Safety: Prevents common bugs without garbage collection overhead
  • Performance: Comparable to C/C++ with high-level ergonomics
  • Concurrency: Excellent async/await support with Tokio
  • Type Safety: Catches errors at compile time
  • Ecosystem: Rich crate ecosystem for JSON, HTTP, databases, etc.

External Learning Resources

Official MCP Documentation:

Rust MCP Toolkit (rmcp):

Rust Learning Resources:

Getting Started

Prerequisites

  • Rust 1.70+ installed
  • Basic understanding of Rust async programming
  • Familiarity with JSON-RPC concepts

Project Setup

# Clone the tutorial repository
git clone <repository-url>
cd mcp-rust-tutorial

# Run any example
cargo run --bin example_01_hello_world

Dependencies

Key dependencies used throughout the tutorial:

[dependencies]
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = { version = "1.0", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }

External Learning Resources

Project Setup and Cargo:

Essential Crates Documentation:

Basic Examples

Example 1: Hello World MCP Server

The simplest possible MCP server with a single greeting tool.

Key Concepts:

  • Basic MCP server structure
  • Tool definitions with JSON schema
  • Simple JSON-RPC message handling

Features Demonstrated:

  • Single tool (greeting) with parameter validation
  • Basic error handling
  • JSON schema for input validation

Rust Concepts Explained:

1. Struct Definition and Implementation Blocks

pub struct HelloWorldServer;  // Unit struct - no fields needed

impl HelloWorldServer {  // Implementation block for methods
    // Methods go here
}
  • Unit Struct: HelloWorldServer is a unit struct (no fields), perfect for stateless servers
  • pub Keyword: Makes the struct public, allowing external access
  • Implementation Block: impl defines methods associated with the struct

2. Vector Creation and Initialization

pub fn list_tools(&self) -> Vec<Tool> {
    vec![Tool { /* ... */ }]  // vec! macro creates a vector
}
  • vec! Macro: Creates a vector with initial elements
  • Return Type: Vec<Tool> specifies a vector of Tool structs
  • &self Parameter: Immutable reference to the struct instance

3. Serde JSON Integration

use serde::{Deserialize, Serialize};  // Import traits

#[derive(Serialize, Deserialize, Debug)]  // Derive macros
pub struct GreetingRequest {
    pub name: String,
}
  • Derive Macros: Automatically implement traits for serialization
  • Serialize/Deserialize: Convert Rust structs to/from JSON
  • Debug Trait: Enables printing with {:?} format

4. JSON Schema with serde_json::json! Macro

input_schema: serde_json::json!({
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "The name of the person to greet"
        }
    },
    "required": ["name"]
}),
  • json! Macro: Creates JSON values at compile time
  • Type Safety: Rust ensures the JSON structure is valid
  • Schema Definition: Defines expected input structure for tools

5. String Handling

name: "greeting".to_string(),  // Convert &str to String
  • String vs &str: String is owned, &str is borrowed
  • .to_string(): Converts string literals to owned String types

Real Code from Example:

1. Data Structures - The Foundation

// Step 1: Define request/response structures for type safety
#[derive(Serialize, Deserialize, Debug)]
pub struct GreetingRequest {
    pub name: String,  // The parameter clients will send
}

#[derive(Serialize, Deserialize, Debug)]
pub struct GreetingResponse {
    pub message: String,  // The formatted response we'll return
}

Pedagogical Note: This demonstrates Rust's approach to data modeling. Instead of working with raw JSON, we define strongly-typed structures that the compiler can validate. The derive macros automatically implement serialization/deserialization.

2. Server Structure - Clean Architecture

// Step 2: Create the server handler (stateless in this simple example)
pub struct HelloWorldServer;

impl HelloWorldServer {
    pub fn new() -> Self {
        Self  // Unit struct constructor
    }

    // Define what tools this server provides
    pub fn list_tools(&self) -> Vec<Tool> {
        vec![Tool {
            name: "greeting".to_string(),
            description: "Generate a personalized greeting message".to_string(),
            input_schema: serde_json::json!({
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string",
                        "description": "The name of the person to greet"
                    }
                },
                "required": ["name"]
            }),
        }]
    }
}

Pedagogical Note: This shows the MCP pattern of separating concerns - the server structure holds state (none in this case), while methods handle specific protocol operations.

3. Tool Implementation - Business Logic

// Step 3: Implement the actual tool logic
pub fn call_tool(&self, name: &str, arguments: Value) -> Result<Value, String> {
    match name {
        "greeting" => {
            // Parse incoming JSON into our typed structure
            let request: GreetingRequest = serde_json::from_value(arguments)
                .map_err(|e| format!("Failed to parse arguments: {}", e))?;

            // Execute business logic (create greeting)
            let response = GreetingResponse {
                message: format!("Hello, {}! Welcome to MCP with Rust!", request.name),
            };

            // Convert back to JSON for MCP protocol
            serde_json::to_value(response)
                .map_err(|e| format!("Failed to serialize response: {}", e))
        }
        _ => Err(format!("Unknown tool: {}", name)),
    }
}

Pedagogical Note: This demonstrates the complete data flow: JSON → Rust struct → business logic → Rust struct → JSON. The ? operator provides clean error propagation.

4. Async Main Function - Program Entry Point

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize logging for debugging
    tracing_subscriber::fmt::init();

    println!("🚀 Starting Hello World MCP Server");
    println!("📝 Available tools: greeting");
    
    let server = HelloWorldServer::new();
    
    // Simple JSON-RPC message loop (production would use rmcp crate)
    use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
    
    let stdin = stdin();
    let mut stdout = stdout();
    let mut reader = BufReader::new(stdin);
    let mut line = String::new();

    loop {
        line.clear();
        match reader.read_line(&mut line).await {
            Ok(0) => break, // EOF
            Ok(_) => {
                // Process JSON-RPC message...
            }
            Err(e) => {
                eprintln!("Error reading input: {}", e);
                break;
            }
        }
    }
    
    Ok(())
}

Pedagogical Note: The #[tokio::main] attribute transforms the main function into an async runtime. This example shows basic I/O handling, though real MCP servers would use the rmcp crate for protocol handling.

Run Example:

cargo run --bin example_01_hello_world

External Learning Resources

Basic Rust Concepts:

JSON and Serialization:

MCP Protocol Basics:

Example 2: Calculator with Error Handling

Building upon the hello world example with mathematical operations and comprehensive error handling.

Key Concepts:

  • Multiple operations in a single tool
  • Custom error types
  • Parameter validation
  • Robust error handling patterns

Features Demonstrated:

  • Mathematical operations (add, subtract, multiply, divide)
  • Division by zero protection
  • Input validation and error messages

Rust Concepts Explained:

1. Result Type and Error Handling

fn perform_calculation(&self, request: &CalculatorRequest) -> Result<f64, CalculatorError> {
    // Result<T, E> represents either success (Ok(T)) or failure (Err(E))
}
  • Result<T, E>: Rust's way of handling fallible operations
  • Ok(value): Success case containing the result
  • Err(error): Failure case containing error information
  • No Exceptions: Rust uses Result instead of exceptions

2. Pattern Matching with match

match request.operation.as_str() {
    "divide" => { /* division logic */ },
    "add" => { /* addition logic */ },
    _ => { /* default case */ }
}
  • Pattern Matching: Exhaustive checking of all possible values
  • String Patterns: Matching on string values
  • Wildcard _: Catches all unmatched cases

3. Custom Error Types

#[derive(Debug)]
pub enum CalculatorError {
    DivisionByZero,
    UnsupportedOperation(String),
}

impl std::fmt::Display for CalculatorError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CalculatorError::DivisionByZero => write!(f, "Division by zero is not allowed"),
            CalculatorError::UnsupportedOperation(op) => write!(f, "Unsupported operation: {}", op),
        }
    }
}
  • Enum: Defines a type with multiple variants
  • Associated Data: UnsupportedOperation(String) carries additional data
  • Trait Implementation: Display trait for user-friendly error messages

4. Conditional Logic and Early Return

if request.b == 0.0 {
    Err(CalculatorError::DivisionByZero)  // Early return with error
} else {
    Ok(request.a / request.b)  // Success case
}
  • Early Return: Return immediately on error conditions
  • Type Safety: Compiler ensures all paths return Result type

5. Method Parameters and References

fn perform_calculation(&self, request: &CalculatorRequest) -> Result<f64, CalculatorError>
//                     ^self    ^borrowed reference
  • &self: Immutable reference to the struct instance
  • &CalculatorRequest: Borrowed reference to avoid ownership transfer
  • Borrowing: Access data without taking ownership

6. Floating Point Operations

request.a / request.b  // f64 division
request.a + request.b  // f64 addition
  • f64 Type: 64-bit floating point numbers
  • Arithmetic Operators: Standard mathematical operations
  • IEEE 754: Rust follows IEEE floating point standards

Real Code from Example:

1. Structured Request/Response Types

// Define the calculator request structure with multiple parameters
#[derive(Serialize, Deserialize, Debug)]
pub struct CalculatorRequest {
    pub operation: String,  // The mathematical operation to perform
    pub a: f64,            // First number in the calculation
    pub b: f64,            // Second number in the calculation
}

#[derive(Serialize, Deserialize, Debug)]
pub struct CalculatorResponse {
    pub result: f64,                    // The result of the calculation
    pub operation_performed: String,    // The operation that was performed (for confirmation)
}

Pedagogical Note: Notice how we model the domain with precise types. The f64 type ensures floating-point arithmetic, while the operation_performed field provides useful feedback to clients.

2. Custom Error Types - Production Pattern

// Custom error type for calculator-specific errors
#[derive(Debug)]
pub enum CalculatorError {
    DivisionByZero,
    UnsupportedOperation(String),
}

impl std::fmt::Display for CalculatorError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CalculatorError::DivisionByZero => write!(f, "Division by zero is not allowed"),
            CalculatorError::UnsupportedOperation(op) => write!(f, "Unsupported operation: {}", op),
        }
    }
}

impl std::error::Error for CalculatorError {}

Pedagogical Note: This demonstrates Rust's idiomatic error handling. By implementing Display and Error traits, our custom errors integrate seamlessly with Rust's error ecosystem.

3. Core Business Logic with Validation

// Private method to perform the actual calculation
fn perform_calculation(&self, request: &CalculatorRequest) -> Result<f64, CalculatorError> {
    match request.operation.as_str() {
        "add" => Ok(request.a + request.b),
        "subtract" => Ok(request.a - request.b),
        "multiply" => Ok(request.a * request.b),
        "divide" => {
            // Validate that we're not dividing by zero
            if request.b == 0.0 {
                Err(CalculatorError::DivisionByZero)
            } else {
                Ok(request.a / request.b)
            }
        }
        _ => Err(CalculatorError::UnsupportedOperation(
            request.operation.clone(),
        )),
    }
}

Pedagogical Note: This shows defensive programming - we validate inputs and handle edge cases explicitly. The match expression ensures all operations are handled, with the _ wildcard catching invalid operations.

4. Tool Definition with Schema Validation

pub fn list_tools(&self) -> Vec<Tool> {
    vec![Tool {
        name: "calculator".to_string(),
        description: "Perform basic arithmetic operations (add, subtract, multiply, divide)".to_string(),
        input_schema: serde_json::json!({
            "type": "object",
            "properties": {
                "operation": {
                    "type": "string",
                    "description": "The operation to perform",
                    "enum": ["add", "subtract", "multiply", "divide"]  // Constrains valid values
                },
                "a": {
                    "type": "number",
                    "description": "First number"
                },
                "b": {
                    "type": "number", 
                    "description": "Second number"
                }
            },
            "required": ["operation", "a", "b"]  // All parameters are mandatory
        }),
    }]
}

Pedagogical Note: The JSON schema provides client-side validation. The enum constraint limits operations to valid values, and required ensures all parameters are provided.

5. Complete Tool Call Handler

pub fn call_tool(&self, name: &str, arguments: Value) -> Result<Value, String> {
    match name {
        "calculator" => {
            // Parse the request
            let request: CalculatorRequest = serde_json::from_value(arguments)
                .map_err(|e| format!("Failed to parse arguments: {}", e))?;

            // Perform the calculation
            let result = self
                .perform_calculation(&request)
                .map_err(|e| e.to_string())?;  // Convert our custom error to string

            // Create the response
            let response = CalculatorResponse {
                result,
                operation_performed: format!(
                    "{} {} {}", 
                    request.a, request.operation, request.b
                ),
            };

            serde_json::to_value(response)
                .map_err(|e| format!("Failed to serialize response: {}", e))
        }
        _ => Err(format!("Unknown tool: {}", name)),
    }
}

Pedagogical Note: This shows the complete error handling pipeline: JSON parsing errors, business logic errors, and serialization errors are all handled gracefully using the ? operator for clean error propagation.

Run Example:

cargo run --bin example_02_calculator

External Learning Resources

Advanced Error Handling:

Pattern Matching:

Floating Point Arithmetic:

Example 3: Text Processor with Multiple Tools

Demonstrates organizing multiple related tools within a single MCP server.

Key Concepts:

  • Multiple tools in one server
  • Text transformation operations
  • Tool organization patterns

Features Demonstrated:

  • Text transformations (uppercase, lowercase, reverse, capitalize)
  • Text analysis (word count, character analysis)
  • Multiple tool management

Rust Concepts Explained:

1. String Methods and Transformations

text.to_uppercase()     // Creates new String with uppercase characters
text.to_lowercase()     // Creates new String with lowercase characters
text.trim().to_string() // Removes whitespace and converts to owned String
  • String Methods: Built-in methods for string manipulation
  • Ownership: These methods create new String instances
  • Method Chaining: Can chain multiple string operations

2. Iterator Patterns and Functional Programming

text.chars().rev().collect()  // Reverse characters using iterators
text.split_whitespace()       // Split into words
    .map(|word| capitalize_word(word))  // Transform each word
    .collect::<Vec<_>>()       // Collect into vector
    .join(" ")                 // Join back with spaces
  • Iterators: Lazy evaluation for efficient processing
  • Functional Style: Transform data through method chains
  • Closures: |word| is a closure (anonymous function)
  • Collect: Materialize iterator results into collections

3. Character Processing

text.chars().any(|c| c.is_uppercase())  // Check if any char is uppercase
text.chars().any(|c| c.is_numeric())    // Check if any char is numeric
text.chars().count()                    // Count characters
  • Character Iterator: .chars() creates iterator over Unicode scalar values
  • Predicate Functions: .any() tests conditions across elements
  • Unicode Support: Full Unicode character support

4. String Splitting and Processing

text.split_whitespace().count()  // Count words
text.lines().count()             // Count lines
  • Split Methods: Various ways to break strings apart
  • Whitespace Handling: Automatic handling of spaces, tabs, newlines

5. Complex String Manipulation

fn capitalize_words(&self, text: &str) -> String {
    text.split_whitespace()
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                None => String::new(),
                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}
  • Option Handling: chars.next() returns Option<char>
  • Pattern Matching: Handle Some/None cases safely
  • String Concatenation: Using + operator for strings
  • Type Annotations: collect::<String>() specifies collection type

Run Example:

cargo run --bin example_03_text_processor

External Learning Resources

String Processing and Iterators:

Unicode and Character Processing:

Functional Programming Patterns:

Example 4: Weather Service

Implementation details would be based on the actual example_04 file

Example 5: Resource Provider

Demonstrates MCP resources for providing document access to LLMs.

Key Concepts:

  • MCP Resources (not just tools)
  • Document storage and retrieval
  • Search functionality
  • URI-based resource identification

Features Demonstrated:

  • Document collection with metadata
  • Search by title, content, author, and tags
  • Resource URIs (document://doc_id)
  • Resource reading by URI

Rust Concepts Explained:

1. HashMap for Data Storage

use std::collections::HashMap;

struct ResourceProviderServer {
    documents: HashMap<String, Document>,  // Key-value storage
}
  • HashMap<K, V>: Hash table for O(1) average lookups
  • Generic Types: K is key type, V is value type
  • Ownership: HashMap owns its key-value pairs

2. Iterator Transformations

self.documents
    .values()           // Iterator over values only
    .map(|doc| Resource { /* transform */ })  // Transform each document
    .collect()          // Materialize into Vec
  • .values(): Iterate over HashMap values, ignoring keys
  • .map(): Transform each element using a closure
  • Lazy Evaluation: Operations are deferred until .collect()

3. Option Types and Pattern Matching

name: Some(doc.title.clone()),          // Wrap in Some variant
description: Some(format!("...")),      // Option<String>
mime_type: Some("text/plain".to_string()),
  • Option: Represents optional values (Some(T) or None)
  • Some(value): Present value variant
  • Explicit Optionality: Rust forces handling of missing values

4. String Formatting and Interpolation

format!("document://{}", doc.id)        // String interpolation
format!("Document by {} - Tags: {}", 
    doc.author, doc.tags.join(", "))    // Multiple parameters
  • format! Macro: Type-safe string formatting
  • {} Placeholders: Positional parameter substitution
  • Display Trait: Uses Display implementation for formatting

5. Vector Operations

doc.tags.join(", ")  // Join vector elements with separator
  • .join(): Concatenate vector elements with delimiter
  • String Collections: Working with Vec<String>

6. Cloning for Ownership

name: Some(doc.title.clone()),  // Clone the string
  • .clone(): Create owned copy of data
  • Ownership Transfer: Move vs clone for memory management
  • Trade-offs: Cloning uses memory but avoids borrowing issues

7. Struct Field Access

Resource {
    uri: format!("document://{}", doc.id),
    name: Some(doc.title.clone()),
    description: Some(format!("Document by {} - Tags: {}", 
        doc.author, doc.tags.join(", "))),
    mime_type: Some("text/plain".to_string()),
}
  • Struct Literals: Creating instances with named fields
  • Field Access: Using dot notation to access struct fields
  • Constructor Pattern: Building complex objects step by step

Real Code from Example:

1. Rich Domain Models - Document Structure

// Structure representing a comprehensive document resource
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Document {
    pub id: String,
    pub title: String,
    pub content: String,
    pub author: String,
    pub created_at: String,
    pub tags: Vec<String>,
}

// MCP Resource representation
#[derive(Serialize, Deserialize, Debug)]
pub struct Resource {
    pub uri: String,              // Unique identifier (e.g., "document://doc1")
    pub name: Option<String>,     // Human-readable name
    pub description: Option<String>, // Detailed description
    pub mime_type: Option<String>,   // Content type
}

Pedagogical Note: Notice the rich metadata model. Documents have structured information, while Resources provide the MCP protocol interface. The Clone trait allows documents to be duplicated when needed.

2. Server with In-Memory Storage

pub struct ResourceProviderServer {
    // In-memory document storage for this example
    // In a real application, this might be a database connection
    documents: HashMap<String, Document>,
}

impl ResourceProviderServer {
    pub fn new() -> Self {
        let mut documents = HashMap::new();

        // Add comprehensive sample documents for testing
        documents.insert("doc1".to_string(), Document {
            id: "doc1".to_string(),
            title: "Introduction to Model Context Protocol".to_string(),
            content: "The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to LLMs...".to_string(),
            author: "MCP Team".to_string(),
            created_at: "2024-01-01T00:00:00Z".to_string(),
            tags: vec!["MCP".to_string(), "Protocol".to_string(), "AI".to_string()],
        });
        // ... more documents

        Self { documents }
    }
}

Pedagogical Note: This shows a common pattern - initializing with sample data for testing. The HashMap provides O(1) lookups by document ID.

3. Resource Listing - Iterator Transformation

// List all available resources
pub fn list_resources(&self) -> Vec<Resource> {
    self.documents
        .values()                    // Iterator over HashMap values
        .map(|doc| Resource {        // Transform each Document into Resource
            uri: format!("document://{}", doc.id),
            name: Some(doc.title.clone()),
            description: Some(format!(
                "Document by {} - Tags: {}", 
                doc.author, 
                doc.tags.join(", ")    // Join vector elements with separator
            )),
            mime_type: Some("text/plain".to_string()),
        })
        .collect()                   // Materialize iterator into Vec
}

Pedagogical Note: This demonstrates functional programming in Rust. The transformation pipeline efficiently converts internal document representation to MCP resource format.

4. Resource Reading with URI Parsing

// Read a specific resource by URI
pub fn read_resource(&self, uri: &str) -> Result<Value, String> {
    // Parse the URI to extract the document ID
    if let Some(doc_id) = uri.strip_prefix("document://") {
        if let Some(document) = self.documents.get(doc_id) {
            // Return the document content as a resource
            Ok(serde_json::json!({
                "contents": [{
                    "uri": uri,
                    "mimeType": "text/plain",
                    "text": document.content
                }]
            }))
        } else {
            Err(format!("Document not found: {}", doc_id))
        }
    } else {
        Err(format!("Invalid document URI: {}", uri))
    }
}

Pedagogical Note: This shows URI parsing and validation. The strip_prefix method safely extracts the document ID, with proper error handling for malformed URIs.

5. Advanced Search Implementation

// Helper method to search documents by query
fn search_documents(&self, query: &str, limit: Option<usize>) -> Vec<&Document> {
    let query_lower = query.to_lowercase();
    let mut matches: Vec<&Document> = self
        .documents
        .values()
        .filter(|doc| {
            doc.title.to_lowercase().contains(&query_lower)
                || doc.content.to_lowercase().contains(&query_lower)
                || doc.author.to_lowercase().contains(&query_lower)
                || doc.tags.iter().any(|tag| tag.to_lowercase().contains(&query_lower))
        })
        .collect();

    // Sort by relevance (simple scoring based on title matches)
    matches.sort_by(|a, b| {
        let a_score = if a.title.to_lowercase().contains(&query_lower) { 2 } else { 0 }
                    + if a.tags.iter().any(|tag| tag.to_lowercase().contains(&query_lower)) { 1 } else { 0 };
        let b_score = if b.title.to_lowercase().contains(&query_lower) { 2 } else { 0 }
                    + if b.tags.iter().any(|tag| tag.to_lowercase().contains(&query_lower)) { 1 } else { 0 };
        b_score.cmp(&a_score)  // Sort by highest score first
    });

    if let Some(limit) = limit {
        matches.into_iter().take(limit).collect()
    } else {
        matches
    }
}

Pedagogical Note: This demonstrates advanced iterator usage with filtering, scoring, and sorting. The search algorithm combines multiple fields with relevance scoring - title matches score higher than tag matches.

6. Tool Integration with Resources

pub fn call_tool(&self, name: &str, arguments: Value) -> Result<Value, String> {
    match name {
        "search_documents" => {
            let request: SearchRequest = serde_json::from_value(arguments)
                .map_err(|e| format!("Failed to parse arguments: {}", e))?;

            let matches = self.search_documents(&request.query, request.limit);

            let response = SearchResponse {
                total_count: matches.len(),
                matches: matches
                    .into_iter()
                    .map(|doc| DocumentSummary {
                        id: doc.id.clone(),
                        title: doc.title.clone(),
                        author: doc.author.clone(),
                        uri: format!("document://{}", doc.id),
                        tags: doc.tags.clone(),
                    })
                    .collect(),
            };

            serde_json::to_value(response)
                .map_err(|e| format!("Failed to serialize response: {}", e))
        }
        _ => Err(format!("Unknown tool: {}", name)),
    }
}

Pedagogical Note: This shows how tools and resources work together. The search tool returns resource URIs that can then be read using the resource protocol.

Run Example:

cargo run --bin example_05_resource_provider

External Learning Resources

Data Structures and Collections:

Option Types and Error Handling:

Search Algorithms and String Processing:

MCP Resources:

Intermediate Examples

Example 6: Configurable Server

Production-ready configuration management using files, environment variables, and command-line arguments.

Key Concepts:

  • Multi-source configuration (files, env vars, CLI args)
  • Tool enablement/disablement
  • Runtime parameter configuration
  • Configuration validation

Features Demonstrated:

  • JSON configuration files
  • Environment variable overrides
  • Command-line argument processing
  • Feature flags for tools

Rust Concepts Explained:

1. Environment Variable Access

use std::env;

if let Ok(server_name) = env::var("MCP_SERVER_NAME") {
    config.server_name = server_name;
}
  • std::env: Standard library module for environment access
  • env::var(): Returns Result<String, VarError>
  • if let Pattern: Convenient pattern matching for Result types
  • Error Handling: Gracefully handle missing environment variables

2. Command Line Argument Processing

let args: Vec<String> = env::args().collect();
for i in 0..args.len() {
    match args[i].as_str() {
        "--server-name" if i + 1 < args.len() => {
            config.server_name = args[i + 1].clone();
        }
        _ => {}
    }
}
  • env::args(): Iterator over command line arguments
  • .collect(): Convert iterator to Vec
  • Index Bounds Checking: Ensure safe array access
  • Pattern Guards: if conditions in match arms

3. Configuration Structure with Defaults

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ServerConfig {
    pub server_name: String,
    pub enabled_tools: Vec<String>,
    pub tool_configs: HashMap<String, ToolConfig>,
}

impl Default for ServerConfig {
    fn default() -> Self {
        // Set sensible defaults
    }
}
  • Derive Macros: Multiple traits derived automatically
  • Clone Trait: Enable config copying
  • Default Trait: Provide sensible default values
  • Nested Structures: Complex configuration hierarchies

4. File I/O and JSON Parsing

if let Ok(config_content) = std::fs::read_to_string("server_config.json") {
    if let Ok(file_config) = serde_json::from_str::<ServerConfig>(&config_content) {
        config = file_config;
    }
}
  • File Operations: Reading entire files to strings
  • Nested Error Handling: Multiple fallible operations
  • Type Annotations: from_str::<ServerConfig> specifies target type
  • Graceful Degradation: Continue with defaults if file missing

5. Configuration Validation

fn validate_configuration(config: &ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
    if config.server_name.is_empty() {
        return Err("Server name cannot be empty".into());
    }
    // More validation...
    Ok(())
}
  • Trait Objects: Box<dyn std::error::Error> for any error type
  • Early Return: Validation with immediate error reporting
  • .into(): Convert string literals to error types

6. Feature Flags and Conditional Logic

for tool_name in &config.enabled_tools {
    if let Some(tool_config) = self.get_tool_config(tool_name) {
        if !tool_config.enabled {
            continue;  // Skip disabled tools
        }
        // Process enabled tool
    }
}
  • Reference Iteration: &config.enabled_tools borrows the vector
  • Option Handling: Safe access to potentially missing values
  • Control Flow: continue to skip loop iterations

Configuration Priority:

  1. Command-line arguments (highest)
  2. Environment variables
  3. Configuration files
  4. Default values (lowest)

Run Example:

# With environment variables
export MCP_SERVER_NAME="My Custom Server"
export MCP_MAX_CONNECTIONS=50
cargo run --bin example_06_configurable_server

# With command line args
cargo run --bin example_06_configurable_server -- --server-name "CLI Server"

External Learning Resources

Configuration Management:

File I/O and JSON Parsing:

Default Trait and Initialization:

Production Configuration:

Example 7: File Operations

Secure file system operations with comprehensive safety controls.

Key Concepts:

  • Path validation and sanitization
  • Directory traversal prevention
  • File size limits
  • Extension filtering
  • Permission management

Features Demonstrated:

  • Safe file reading/writing
  • Directory listing with metadata
  • File information retrieval
  • Configurable security policies

Security Features:

  • Allowed directory restrictions
  • File extension whitelisting
  • Path canonicalization
  • Size limit enforcement
  • Read-only mode support

Run Example:

cargo run --bin example_07_file_operations

Example 8: HTTP Client

Implementation details would be based on the actual example_08 file

Example 9: Database Integration

SQLite database integration with connection pooling and migrations.

Key Concepts:

  • Database connection pooling
  • SQL migrations
  • CRUD operations
  • Prepared statements
  • Error handling for database operations

Features Demonstrated:

  • User management (create, read, update, delete)
  • Database migrations
  • Search with pagination
  • Connection pool management
  • Operation logging

Rust Concepts Explained:

1. Async/Await Programming

async fn create_user(&self, arguments: Value) -> Result<Value, String> {
    // async function returns Future<Output = Result<Value, String>>
    let result = sqlx::query_as::<_, (i64,)>(...)
        .await?;  // await the future and propagate errors
}
  • async fn: Declares an asynchronous function
  • await: Suspends execution until future completes
  • Non-blocking: Other tasks can run while waiting for I/O
  • Error Propagation: ? operator works with async functions

2. Connection Pooling with SqlitePool

use sqlx::SqlitePool;

pub struct DatabaseServer {
    pool: SqlitePool,  // Shared connection pool
}

let pool = SqlitePool::connect("sqlite:./data/example.db").await?;
  • Connection Pooling: Reuse database connections efficiently
  • Async Operations: All database calls are async
  • Resource Management: Pool handles connection lifecycle
  • Concurrent Access: Multiple tasks can share the pool safely

3. Prepared Statements and Parameter Binding

sqlx::query_as::<_, (i64,)>(
    "INSERT INTO users (name, email, age) VALUES (?, ?, ?) RETURNING id"
)
.bind(&request.name)    // Bind first parameter
.bind(&request.email)   // Bind second parameter
.bind(request.age)      // Bind third parameter
  • SQL Injection Protection: Parameters are safely escaped
  • Type Safety: Compile-time checking of SQL types
  • Parameter Binding: .bind() method for each placeholder
  • Query Compilation: Statements are prepared once and reused

4. Tuple Destructuring and Type Annotations

sqlx::query_as::<_, (i64,)>(...)  // Returns tuple with single i64
let result = result.0;            // Extract the ID from tuple
  • Type Hints: <_, (i64,)> specifies return type
  • Tuple Types: (i64,) is a single-element tuple
  • Destructuring: Access tuple elements by index

5. Error Handling with map_err

.fetch_one(&self.pool)
.await
.map_err(|e| format!("Failed to create user: {}", e))?;
  • Error Transformation: Convert database errors to strings
  • .map_err(): Transform error type while preserving success value
  • Error Context: Add meaningful error messages
  • Chaining: Combine with ? operator for propagation

6. JSON Deserialization

let request: CreateUserRequest = serde_json::from_value(arguments)?;
  • Type Inference: Rust infers the target type from annotation
  • Automatic Validation: Serde validates JSON structure
  • Error Propagation: ? converts serde errors to function error type

7. Database Migrations

sqlx::query(
    r#"
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL,
        created_at TEXT NOT NULL DEFAULT (datetime('now'))
    )
    "#,
)
.execute(&self.pool)
.await?;
  • Raw String Literals: r#"..."# preserves formatting and escaping
  • DDL Operations: Data Definition Language for schema changes
  • Idempotent Migrations: IF NOT EXISTS for safe re-runs

Real Code from Example:

1. Database Configuration and Connection Setup

use sqlx::{Sqlite, SqlitePool};

#[derive(Debug, Serialize, Deserialize)]
pub struct DatabaseConfig {
    pub database_url: String,
    pub max_connections: u32,
}

impl Default for DatabaseConfig {
    fn default() -> Self {
        Self {
            database_url: "sqlite:./data/example.db".to_string(),
            max_connections: 10,
        }
    }
}

pub struct DatabaseServer {
    pool: SqlitePool,  // Connection pool for efficient database access
    config: DatabaseConfig,
}

Pedagogical Note: Connection pooling is crucial for database performance. SQLx provides async connection pooling out of the box, allowing multiple concurrent database operations.

2. Async Database Initialization with Migrations

impl DatabaseServer {
    pub async fn new(config: DatabaseConfig) -> Result<Self, sqlx::Error> {
        // Create the data directory if it doesn't exist
        if let Some(parent) = std::path::Path::new(&config.database_url.replace("sqlite:", "")).parent() {
            std::fs::create_dir_all(parent).map_err(|e| sqlx::Error::Io(e))?;
        }

        // Create connection pool with configuration
        let pool = SqlitePool::connect_with(
            sqlx::sqlite::SqliteConnectOptions::new()
                .filename(config.database_url.replace("sqlite:", ""))
                .create_if_missing(true)
                .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal),
        )
        .await?;

        let server = Self { pool, config };

        // Run database migrations
        server.run_migrations().await?;

        Ok(server)
    }

    async fn run_migrations(&self) -> Result<(), sqlx::Error> {
        // Create users table if it doesn't exist
        sqlx::query(
            r#"
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL,
                age INTEGER,
                created_at TEXT NOT NULL DEFAULT (datetime('now'))
            )
            "#,
        )
        .execute(&self.pool)
        .await?;

        println!("✅ Database migrations completed successfully");
        Ok(())
    }
}

Pedagogical Note: This shows production-ready database setup with migrations, WAL mode for better concurrency, and proper error handling. The create_if_missing option simplifies deployment.

3. Type-Safe Database Operations

#[derive(Serialize, Deserialize, Debug)]
pub struct CreateUserRequest {
    pub name: String,
    pub email: String,
    pub age: Option<i32>,
}

#[derive(Serialize, Deserialize, Debug, sqlx::FromRow)]
pub struct User {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub age: Option<i32>,
    pub created_at: String,
}

async fn create_user(&self, arguments: Value) -> Result<Value, String> {
    let request: CreateUserRequest = serde_json::from_value(arguments)
        .map_err(|e| format!("Failed to parse arguments: {}", e))?;

    // Insert user and return the generated ID
    let result = sqlx::query_as::<_, (i64,)>(
        "INSERT INTO users (name, email, age) VALUES (?, ?, ?) RETURNING id"
    )
    .bind(&request.name)
    .bind(&request.email)
    .bind(request.age)
    .fetch_one(&self.pool)
    .await
    .map_err(|e| format!("Failed to create user: {}", e))?;

    let user_id = result.0;

    // Fetch the complete user record
    let user = sqlx::query_as::<_, User>(
        "SELECT id, name, email, age, created_at FROM users WHERE id = ?"
    )
    .bind(user_id)
    .fetch_one(&self.pool)
    .await
    .map_err(|e| format!("Failed to fetch created user: {}", e))?;

    serde_json::to_value(&user)
        .map_err(|e| format!("Failed to serialize user: {}", e))
}

Pedagogical Note: The sqlx::FromRow derive macro automatically maps SQL rows to Rust structs. Parameter binding prevents SQL injection attacks while maintaining type safety.

4. Advanced Query with Pagination

#[derive(Serialize, Deserialize, Debug)]
pub struct SearchUsersRequest {
    pub query: Option<String>,
    pub page: Option<u32>,
    pub page_size: Option<u32>,
}

async fn search_users(&self, arguments: Value) -> Result<Value, String> {
    let request: SearchUsersRequest = serde_json::from_value(arguments)?;
    
    let page = request.page.unwrap_or(1);
    let page_size = request.page_size.unwrap_or(10).min(100); // Limit max page size
    let offset = (page - 1) * page_size;

    let users = if let Some(query) = &request.query {
        // Search with filtering
        sqlx::query_as::<_, User>(
            "SELECT id, name, email, age, created_at FROM users 
             WHERE name LIKE ? OR email LIKE ? 
             ORDER BY created_at DESC 
             LIMIT ? OFFSET ?"
        )
        .bind(format!("%{}%", query))
        .bind(format!("%{}%", query))
        .bind(page_size as i32)
        .bind(offset as i32)
        .fetch_all(&self.pool)
        .await
    } else {
        // Get all users with pagination
        sqlx::query_as::<_, User>(
            "SELECT id, name, email, age, created_at FROM users 
             ORDER BY created_at DESC 
             LIMIT ? OFFSET ?"
        )
        .bind(page_size as i32)
        .bind(offset as i32)
        .fetch_all(&self.pool)
        .await
    }
    .map_err(|e| format!("Failed to search users: {}", e))?;

    // Get total count for pagination
    let total_count: (i64,) = if let Some(query) = &request.query {
        sqlx::query_as(
            "SELECT COUNT(*) FROM users WHERE name LIKE ? OR email LIKE ?"
        )
        .bind(format!("%{}%", query))
        .bind(format!("%{}%", query))
        .fetch_one(&self.pool)
        .await
    } else {
        sqlx::query_as("SELECT COUNT(*) FROM users")
            .fetch_one(&self.pool)
            .await
    }
    .map_err(|e| format!("Failed to count users: {}", e))?;

    let response = serde_json::json!({
        "users": users,
        "page": page,
        "page_size": page_size,
        "total_count": total_count.0,
        "total_pages": (total_count.0 as f64 / page_size as f64).ceil() as u32
    });

    Ok(response)
}

Pedagogical Note: This demonstrates real-world database patterns: pagination, search filtering, and count queries. The LIKE operator provides simple text searching, while LIMIT/OFFSET handles pagination.

5. Transaction Support for Data Integrity

async fn update_user(&self, arguments: Value) -> Result<Value, String> {
    let request: UpdateUserRequest = serde_json::from_value(arguments)?;
    
    // Start a database transaction
    let mut tx = self.pool.begin().await
        .map_err(|e| format!("Failed to start transaction: {}", e))?;

    // Update the user within the transaction
    let rows_affected = sqlx::query(
        "UPDATE users SET name = ?, email = ?, age = ? WHERE id = ?"
    )
    .bind(&request.name)
    .bind(&request.email)
    .bind(request.age)
    .bind(request.id)
    .execute(&mut *tx)
    .await
    .map_err(|e| format!("Failed to update user: {}", e))?
    .rows_affected();

    if rows_affected == 0 {
        // Rollback transaction if user not found
        tx.rollback().await
            .map_err(|e| format!("Failed to rollback transaction: {}", e))?;
        return Err("User not found".to_string());
    }

    // Fetch the updated user
    let user = sqlx::query_as::<_, User>(
        "SELECT id, name, email, age, created_at FROM users WHERE id = ?"
    )
    .bind(request.id)
    .fetch_one(&mut *tx)
    .await
    .map_err(|e| format!("Failed to fetch updated user: {}", e))?;

    // Commit the transaction
    tx.commit().await
        .map_err(|e| format!("Failed to commit transaction: {}", e))?;

    serde_json::to_value(&user)
        .map_err(|e| format!("Failed to serialize user: {}", e))
}

Pedagogical Note: Transactions ensure data integrity. If any operation fails, the entire transaction is rolled back. The mut *tx syntax allows using the transaction reference with SQLx queries.

Run Example:

cargo run --bin example_09_database

External Learning Resources

Database Programming in Rust:

SQL and Database Concepts:

Async Rust and Tokio:

Production Database Patterns:

Example 10: WebSocket Server

Implementation details would be based on the actual example_10 file

Advanced Examples

Example 11: Monitoring and Metrics

Comprehensive system monitoring with metrics collection, health checks, and alerting.

Key Concepts:

  • Real-time metrics collection
  • Health check orchestration
  • Threshold-based alerting
  • Historical data management
  • Time-series data handling

Features Demonstrated:

  • System metrics (CPU, memory, disk, network)
  • Service health checks
  • Alert management (creation, filtering, clearing)
  • Metrics history with circular buffering
  • Configurable alert thresholds

Monitoring Capabilities:

  • CPU and memory usage tracking
  • Network activity monitoring
  • Service availability checks
  • Alert threshold configuration
  • Historical trend analysis

Run Example:

cargo run --bin example_11_monitoring

Example 12: Task Queue System

Async background task processing with priority queues and worker management.

Key Concepts:

  • Priority-based task scheduling
  • Background worker processes
  • Channel-based communication
  • Graceful shutdown handling
  • Task retry mechanisms

Features Demonstrated:

  • Task prioritization (Low, Normal, High, Critical)
  • Async task execution
  • Worker lifecycle management
  • Task status tracking
  • Error handling and logging

Rust Concepts Explained:

1. Generic Functions with Trait Bounds

pub async fn add_task<F>(
    &self,
    priority: TaskPriority,
    task: F,
    description: String,
) -> Result<u64, String>
where
    F: Fn() -> Result<String, String> + Send + 'static,
  • Generic Parameters: <F> introduces a type parameter
  • Trait Bounds: Fn() + Send + 'static constrains the type
  • Closure Traits: Fn() means the closure can be called multiple times
  • Thread Safety: Send allows moving between threads
  • Lifetime: 'static means no borrowed references with shorter lifetimes

2. Channels for Inter-Task Communication

use tokio::sync::mpsc;

let (sender, mut receiver) = mpsc::unbounded_channel::<TaskItem>();
  • Multi-Producer Single-Consumer: Multiple senders, one receiver
  • Unbounded Channel: No limit on queued messages
  • Generic Channel: <TaskItem> specifies message type
  • Async Communication: Non-blocking message passing

3. Enums with Ordering

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum TaskPriority {
    Low = 1,
    Normal = 2,
    High = 3,
    Critical = 4,
}
  • Discriminant Values: Explicit numeric values for ordering
  • Derive Traits: Automatic comparison implementation
  • Ordering: PartialOrd and Ord enable sorting
  • Copy Semantics: Lightweight enum copying

4. Boxed Closures and Dynamic Dispatch

type TaskFunction = Box<dyn Fn() -> Result<String, String> + Send>;

pub struct TaskItem {
    task: TaskFunction,  // Dynamically dispatched function
}
  • Trait Objects: dyn Fn() for runtime polymorphism
  • Heap Allocation: Box<> stores closures on the heap
  • Type Aliases: type TaskFunction creates readable type names
  • Send Trait: Ensures thread safety for cross-thread transfer

5. Background Task Spawning

tokio::spawn(async move {
    let mut buffer = VecDeque::new();
    
    while let Some(task) = receiver.recv().await {
        buffer.push_back(task);
        buffer.sort_by(|a, b| b.priority.cmp(&a.priority));
        // Process tasks...
    }
});
  • Task Spawning: tokio::spawn creates concurrent task
  • Move Semantics: async move transfers ownership into closure
  • Deque Operations: VecDeque for efficient queue operations
  • Custom Sorting: Priority-based task ordering

6. Error Handling in Channels

self.sender.send(task_item)
    .map_err(|_| "Task queue is shut down".to_string())?;
  • Channel Errors: Send fails when receiver is dropped
  • Error Mapping: Convert channel error to string
  • Graceful Degradation: Meaningful error messages

7. Async Control Flow

while let Some(task) = receiver.recv().await {
    // Process each task as it arrives
    let task_id = task.id;
    
    // Execute the task and handle the result
    match (task.task)() {
        Ok(result) => println!("Task {} completed: {}", task_id, result),
        Err(error) => println!("Task {} failed: {}", task_id, error),
    }
}
  • Async Loops: while let with .await for stream processing
  • Pattern Matching: Handle task execution results
  • Non-blocking: Other tasks can run during await points

Real Code from Example:

1. Type System for Async Tasks

// Type alias for task functions
// This represents a task that can be executed asynchronously
// Tasks are boxed functions that return a Result
type Task = Box<dyn Fn() -> Result<String, String> + Send + 'static>;

// Enum: TaskPriority with explicit ordering
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum TaskPriority {
    Low = 1,
    Normal = 2,
    High = 3,
    Critical = 4,
}

// Task container with metadata
pub struct TaskItem {
    id: u64,
    priority: TaskPriority,
    task: Task,
    description: String,
}

Pedagogical Note: The type alias makes complex types readable. The Send + 'static bounds ensure tasks can be moved between threads safely. The Ord trait enables priority-based sorting.

2. Queue Structure with Channel Communication

use tokio::sync::{mpsc, Mutex, Notify};
use std::collections::VecDeque;

pub struct TaskQueue {
    sender: mpsc::UnboundedSender<TaskItem>,    // Send tasks to worker
    shutdown_notify: Arc<Notify>,                // Coordinate shutdown
    next_task_id: Arc<Mutex<u64>>,              // Thread-safe ID generation
}

impl TaskQueue {
    pub fn new() -> Self {
        // Create an unbounded channel for task communication
        let (sender, receiver) = mpsc::unbounded_channel::<TaskItem>();
        
        // Create a notification mechanism for graceful shutdown
        let shutdown_notify = Arc::new(Notify::new());
        let shutdown_notify_worker = shutdown_notify.clone();
        
        // Initialize the task ID counter
        let next_task_id = Arc::new(Mutex::new(1u64));

        // Spawn the background worker task
        tokio::spawn(async move {
            Self::worker_loop(receiver, shutdown_notify_worker).await;
        });

        Self { sender, shutdown_notify, next_task_id }
    }
}

Pedagogical Note: This shows the async ecosystem in action. mpsc::unbounded_channel provides async communication, Notify enables graceful shutdown signaling, and Arc<Mutex<>> provides thread-safe shared state.

3. Advanced Async Task Management

pub async fn add_task<F>(
    &self,
    priority: TaskPriority,
    task: F,
    description: String,
) -> Result<u64, String>
where
    F: Fn() -> Result<String, String> + Send + 'static,
{
    // Generate a unique ID for this task
    let mut next_id = self.next_task_id.lock().await;
    let task_id = *next_id;
    *next_id += 1;
    drop(next_id); // Release the lock early

    // Create the task item
    let task_item = TaskItem::new(task_id, priority, Box::new(task), description.clone());

    // Send the task to the worker
    match self.sender.send(task_item) {
        Ok(_) => {
            info!("Queued task {}: {} (priority: {:?})", task_id, description, priority);
            Ok(task_id)
        }
        Err(_) => {
            error!("Failed to queue task: worker has shut down");
            Err("Task queue is shut down".to_string())
        }
    }
}

Pedagogical Note: Notice the careful lock management - we acquire the mutex, increment the counter, then immediately drop the lock to minimize contention. The channel send operation can fail if the receiver is dropped.

4. Priority-Based Worker Loop

async fn worker_loop(
    mut receiver: mpsc::UnboundedReceiver<TaskItem>,
    shutdown_notify: Arc<Notify>,
) {
    // Use a priority queue to ensure high-priority tasks are executed first
    let mut task_buffer: VecDeque<TaskItem> = VecDeque::new();

    info!("Task queue worker started");

    loop {
        // Use tokio::select! to handle both incoming tasks and shutdown signals
        tokio::select! {
            // Handle incoming tasks
            task_option = receiver.recv() => {
                match task_option {
                    Some(task) => {
                        // Insert the task in priority order
                        Self::insert_task_by_priority(&mut task_buffer, task);
                        
                        // Process all available tasks in the buffer
                        Self::process_task_buffer(&mut task_buffer).await;
                    }
                    None => {
                        // Channel closed, no more tasks will arrive
                        warn!("Task channel closed, worker shutting down");
                        break;
                    }
                }
            }
            
            // Handle shutdown signal
            _ = shutdown_notify.notified() => {
                info!("Shutdown signal received, processing remaining tasks");
                
                // Process any remaining tasks
                Self::process_task_buffer(&mut task_buffer).await;
                
                // Process any remaining tasks in the channel
                while let Ok(task) = receiver.try_recv() {
                    Self::insert_task_by_priority(&mut task_buffer, task);
                }
                Self::process_task_buffer(&mut task_buffer).await;
                
                info!("Worker shutdown complete");
                break;
            }
        }
    }
}

Pedagogical Note: tokio::select! is crucial for async programming - it allows handling multiple async operations concurrently. The worker processes tasks in priority order and handles graceful shutdown.

5. Priority Queue Implementation

// Insert a task into the buffer maintaining priority order
fn insert_task_by_priority(buffer: &mut VecDeque<TaskItem>, task: TaskItem) {
    // Find the correct position to insert the task based on priority
    let insert_position = buffer
        .iter()
        .position(|existing_task| existing_task.priority < task.priority)
        .unwrap_or(buffer.len());

    buffer.insert(insert_position, task);
}

// Process all tasks currently in the buffer
async fn process_task_buffer(buffer: &mut VecDeque<TaskItem>) {
    while let Some(task) = buffer.pop_front() {
        let task_id = task.id;

        // Execute the task and handle the result
        match task.execute() {
            Ok(result) => {
                info!("Task {} completed successfully: {}", task_id, result);
            }
            Err(error) => {
                error!("Task {} failed: {}", task_id, error);
            }
        }

        // Add a small delay between tasks to prevent overwhelming the system
        sleep(Duration::from_millis(10)).await;
    }
}

Pedagogical Note: This shows manual priority queue implementation using VecDeque. Tasks are inserted in priority order and processed sequentially. The small delay prevents CPU saturation.

6. Sample Task Creation and Usage

// Create a sample task function for demonstration
fn create_sample_task(
    task_name: String,
    work_duration_ms: u64,
    should_fail: bool,
) -> Box<dyn Fn() -> Result<String, String> + Send + 'static> {
    Box::new(move || {
        // Simulate some work
        std::thread::sleep(Duration::from_millis(work_duration_ms));

        if should_fail {
            Err(format!("Task '{}' failed as requested", task_name))
        } else {
            Ok(format!("Task '{}' completed after {}ms", task_name, work_duration_ms))
        }
    })
}

// Usage in main function
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let task_queue = TaskQueue::new();

    // Add a high-priority task
    task_queue.add_task(
        TaskPriority::High,
        create_sample_task("High Priority Task".to_string(), 100, false),
        "Critical system maintenance".to_string(),
    ).await?;

    // Add a critical priority task (should be processed first)
    task_queue.add_task(
        TaskPriority::Critical,
        create_sample_task("Critical Task".to_string(), 75, false),
        "Emergency response".to_string(),
    ).await?;

    Ok(())
}

Pedagogical Note: This demonstrates closure creation with move semantics. The move keyword transfers ownership of variables into the closure, making it 'static. The higher-level API makes task creation simple.

Run Example:

cargo run --bin example_12_task_queue

External Learning Resources

Advanced Async Programming:

Concurrency Patterns:

Generic Programming and Trait Bounds:

Production Task Queue Systems:

Example 13: Authentication Service

Production-ready authentication with JWT tokens, password hashing, and session management.

Key Concepts:

  • Secure password hashing (SHA-256 for demo, use bcrypt/Argon2 in production)
  • JWT-like token management
  • Role-based access control
  • Account lockout protection
  • Session lifecycle management

Features Demonstrated:

  • User registration with validation
  • Secure authentication
  • Token generation and validation
  • Account lockout after failed attempts
  • Role-based permissions

Rust Concepts Explained:

1. Thread-Safe Shared State

use std::sync::{Arc, RwLock};

pub struct AuthService {
    users: Arc<RwLock<HashMap<String, User>>>,
    active_tokens: Arc<RwLock<HashMap<String, TokenInfo>>>,
}
  • Arc (Atomically Reference Counted): Enables shared ownership across threads
  • RwLock: Multiple readers OR single writer lock
  • Thread Safety: Safe concurrent access to shared data
  • Interior Mutability: Modify data behind shared references

2. Enums for Type Safety

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum UserRole {
    Admin,
    User,
    Guest,
}

#[derive(Debug)]
pub enum AuthError {
    UserNotFound,
    InvalidPassword,
    AccountLocked,
    TokenExpired,
}
  • Type Safety: Compile-time guarantees for valid values
  • Pattern Matching: Exhaustive handling of all cases
  • Serialization: Convert to/from JSON with serde
  • Error Variants: Structured error handling

3. DateTime Handling with Chrono

use chrono::{DateTime, Duration, Utc};

expires_at: Utc::now() + Duration::hours(24),
last_login: Some(Utc::now()),
  • UTC Timestamps: Timezone-aware datetime handling
  • Duration Arithmetic: Add/subtract time periods
  • Type Safety: Compile-time checks for datetime operations
  • Serialization: JSON-compatible datetime formats

4. Password Hashing and Security

use sha2::{Digest, Sha256};

fn hash_password(password: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(password.as_bytes());
    hex::encode(hasher.finalize())
}
  • Cryptographic Hashing: One-way password transformation
  • Byte Operations: Convert strings to bytes for hashing
  • Hex Encoding: Convert binary hash to string representation
  • Security Note: Use bcrypt/Argon2 in production

5. Token Generation and Validation

fn generate_token(&self) -> String {
    format!("{}_{}", 
        Uuid::new_v4().to_string().replace('-', ""),
        Utc::now().timestamp()
    )
}

fn is_token_valid(&self, token: &str) -> bool {
    if let Ok(tokens) = self.active_tokens.read() {
        if let Some(token_info) = tokens.get(token) {
            return token_info.expires_at > Utc::now();
        }
    }
    false
}
  • UUID Generation: Cryptographically secure random IDs
  • String Manipulation: Format and modify token strings
  • Lock Acquisition: Safe access to shared data
  • Nested Pattern Matching: Handle multiple Option/Result types

6. Account Lockout Logic

if user.failed_login_attempts >= 3 {
    if let Some(lockout_until) = user.locked_until {
        if Utc::now() < lockout_until {
            return Err(AuthError::AccountLocked);
        }
    }
}
  • Conditional Logic: Complex security rule implementation
  • Option Handling: Safe access to potentially missing values
  • Time Comparisons: Validate lockout periods
  • Early Return: Immediate rejection of locked accounts

7. RwLock Usage Patterns

// Reading data
let users = self.users.read().unwrap();
if let Some(user) = users.get(username) { /* ... */ }

// Writing data
let mut users = self.users.write().unwrap();
users.insert(username.to_string(), new_user);
  • Read Lock: Multiple concurrent readers
  • Write Lock: Exclusive access for modifications
  • Lock Acquisition: .read() and .write() methods
  • Panic Handling: .unwrap() for lock poisoning (use proper error handling in production)

Security Features:

  • Password strength validation
  • Failed attempt tracking
  • Account lockout mechanism
  • Token expiration handling
  • Secure session management

Run Example:

cargo run --bin example_13_auth_service

External Learning Resources

Security and Cryptography:

Thread Safety and Concurrency:

Authentication Patterns:

DateTime and Temporal Logic:

Production Authentication:

Example 14: Notification Service

Multi-channel notification system with templates and delivery tracking.

Key Concepts:

  • Multi-channel delivery (Email, SMS, Webhook, Push)
  • Template-based messaging
  • Subscription management
  • Delivery tracking and retry logic
  • Background worker processes

Features Demonstrated:

  • Notification templates with variables
  • User subscription management
  • Multi-channel delivery
  • Delivery status tracking
  • Retry mechanisms for failed deliveries

Supported Channels:

  • Email notifications
  • SMS messaging
  • Webhook callbacks
  • Push notifications
  • In-app notifications

Run Example:

cargo run --bin example_14_notification_service

Example 15: Data Pipeline

ETL (Extract, Transform, Load) pipeline with data processing and transformation.

Key Concepts:

  • Data transformation pipelines
  • ETL operations
  • Data validation
  • Pipeline composition
  • Error handling in data flows

Features Demonstrated:

  • Data record processing
  • Transformation operations (filter, map, enrich)
  • Pipeline chaining
  • Error tracking and statistics
  • Data validation

Transformation Types:

  • Filtering by field values
  • Mathematical transformations
  • Data enrichment
  • Statistical operations

Run Example:

cargo run --bin example_15_data_pipeline

Enterprise Examples

Example 16: Search Service

Full-text search engine with indexing and relevance scoring.

Key Concepts:

  • Document indexing
  • Full-text search
  • Relevance scoring
  • Search result ranking
  • Index management

Features Demonstrated:

  • Document indexing with metadata
  • Word-based search indexing
  • Tag-based searching
  • Relevance scoring algorithms
  • Search result pagination

Run Example:

cargo run --bin example_16_search_service

Example 17: Blockchain Integration

Blockchain concepts including blocks, transactions, and proof-of-work.

Key Concepts:

  • Blockchain data structures
  • Transaction management
  • Hash-based security
  • Proof-of-work mining
  • Chain validation

Features Demonstrated:

  • Block creation and mining
  • Transaction processing
  • Hash calculation with SHA-256
  • Proof-of-work algorithm
  • Balance tracking

Run Example:

cargo run --bin example_17_blockchain_integration

Example 18: ML Model Server

Machine learning model serving with inference capabilities.

Key Concepts:

  • Model management
  • Inference pipelines
  • Batch prediction
  • Model versioning
  • Performance tracking

Features Demonstrated:

  • Model registration and activation
  • Single and batch predictions
  • Model versioning
  • Inference statistics
  • Model lifecycle management

Run Example:

cargo run --bin example_18_ml_model_server

Example 19: Microservice Gateway

Service mesh gateway with routing and load balancing.

Key Concepts:

  • Service discovery
  • Load balancing strategies
  • Request routing
  • Health checking
  • Gateway patterns

Features Demonstrated:

  • Service registration
  • Round-robin load balancing
  • Route mapping
  • Health status monitoring
  • Request/response tracking

Load Balancing Strategies:

  • Round Robin
  • Weighted Round Robin
  • Random selection

Run Example:

cargo run --bin example_19_microservice_gateway

Example 20: Enterprise Server

Complete enterprise application combining authentication, monitoring, caching, and APIs.

Key Concepts:

  • Enterprise architecture patterns
  • Component integration
  • Caching strategies
  • API management
  • Production patterns

Features Demonstrated:

  • User management with sessions
  • Multi-layer caching
  • API endpoint routing
  • Metrics collection
  • Enterprise security patterns

Rust Concepts Explained:

1. Complex Generic Structures

pub struct Cache<T: Clone> {
    entries: Arc<RwLock<HashMap<String, CacheEntry<T>>>>,
}

struct CacheEntry<T> {
    value: T,
    expires_at: DateTime<Utc>,
}
  • Generic Structures: <T: Clone> makes cache work with any cloneable type
  • Trait Bounds: Clone constraint enables value duplication
  • Nested Generics: CacheEntry<T> uses the same generic parameter
  • Composition: Complex data structures built from simpler ones

2. Advanced Arc and RwLock Patterns

pub struct EnterpriseServer {
    users: Arc<RwLock<HashMap<Uuid, User>>>,
    sessions: Arc<RwLock<HashMap<Uuid, Session>>>,
    user_cache: Cache<User>,
    metrics: Arc<RwLock<Metrics>>,
}
  • Multiple Shared Resources: Each field is independently lockable
  • Lock Granularity: Fine-grained locking prevents contention
  • Type Composition: Combine primitive and custom types
  • Resource Management: Automatic cleanup with RAII

3. TTL (Time-To-Live) Implementation

pub fn get(&self, key: &str) -> Option<T> {
    if let Ok(entries) = self.entries.read() {
        if let Some(entry) = entries.get(key) {
            if Utc::now() < entry.expires_at {
                return Some(entry.value.clone());
            }
        }
    }
    None
}
  • Expiration Logic: Check current time against expiration
  • Automatic Cleanup: Expired entries are ignored
  • Clone Semantics: Return owned copies of cached values
  • Thread Safety: Multiple readers can access cache concurrently

4. Builder Pattern and Fluent APIs

impl<T: Clone> Cache<T> {
    pub fn new() -> Self { /* ... */ }
    
    pub fn set(&self, key: String, value: T, ttl_seconds: u64) {
        let expires_at = Utc::now() + Duration::seconds(ttl_seconds as i64);
        // Insert with expiration...
    }
}
  • Method Chaining: Fluent interface design
  • Duration Calculations: Compute expiration times
  • Type Conversion: Safe casting between numeric types
  • Immutable API: Methods take &self for concurrent access

5. Metrics Collection and Aggregation

#[derive(Debug, Default)]
pub struct Metrics {
    request_count: u64,
    total_response_time: u64,
    active_sessions: u32,
    cache_hits: u64,
    cache_misses: u64,
}

impl Metrics {
    pub fn average_response_time(&self) -> f64 {
        if self.request_count > 0 {
            self.total_response_time as f64 / self.request_count as f64
        } else {
            0.0
        }
    }
}
  • Default Trait: Zero-initialized metrics
  • Numeric Calculations: Average computation with division
  • Type Casting: Convert integers to floating point
  • Division by Zero: Safe handling of edge cases

6. Session Management

pub fn create_session(&self, user_id: Uuid) -> Result<Uuid, String> {
    let session = Session {
        id: Uuid::new_v4(),
        user_id,
        created_at: Utc::now(),
        expires_at: Utc::now() + Duration::hours(24),
        last_accessed: Utc::now(),
    };
    
    if let Ok(mut sessions) = self.sessions.write() {
        sessions.insert(session.id, session);
        Ok(session.id)
    } else {
        Err("Failed to create session".to_string())
    }
}
  • Struct Initialization: Named field syntax
  • UUID Generation: Unique session identifiers
  • Duration Arithmetic: Session expiration calculation
  • Error Handling: Graceful failure with meaningful messages

7. Advanced Pattern Matching

match request.path.as_str() {
    "/api/users" => match request.method.as_str() {
        "GET" => self.list_users(&request),
        "POST" => self.create_user(&request),
        _ => self.method_not_allowed(),
    },
    path if path.starts_with("/api/users/") => {
        let user_id = path.strip_prefix("/api/users/").unwrap();
        self.get_user(user_id, &request)
    },
    _ => self.not_found(),
}
  • Nested Matching: Match patterns inside match arms
  • Pattern Guards: if conditions in match arms
  • String Methods: .starts_with() and .strip_prefix()
  • Route Parsing: Extract parameters from URL paths

Enterprise Features:

  • Session-based authentication
  • TTL-based caching
  • API rate limiting concepts
  • Comprehensive monitoring
  • Production-ready error handling

Run Example:

cargo run --bin example_20_enterprise_server

Rust Concepts Summary

The examples in this tutorial demonstrate a comprehensive range of Rust language features and patterns essential for MCP development:

Core Language Features

1. Ownership and Borrowing

  • Ownership Transfer: Moving values between functions
  • Borrowing: & for immutable references, &mut for mutable references
  • Lifetimes: Ensuring references are valid ('static lifetime)
  • RAII: Automatic resource cleanup when values go out of scope

2. Type System and Safety

  • Strong Typing: Compile-time type checking prevents runtime errors
  • Option: Safe handling of potentially missing values
  • Result<T, E>: Explicit error handling without exceptions
  • Enums: Type-safe unions with pattern matching
  • Generics: Code reuse with type parameters and trait bounds

3. Pattern Matching

  • match Expressions: Exhaustive handling of all cases
  • if let: Convenient pattern matching for single cases
  • while let: Pattern matching in loops
  • Pattern Guards: Additional conditions in match arms

Memory Management

1. Smart Pointers

  • Box: Heap allocation for owned data
  • Arc: Atomic reference counting for shared ownership
  • Rc: Reference counting for single-threaded sharing

2. Interior Mutability

  • RwLock: Multiple readers or single writer
  • Mutex: Mutual exclusion for shared mutable state
  • Cell and RefCell: Single-threaded interior mutability

Concurrency and Async Programming

1. Async/Await

  • async fn: Asynchronous function declarations
  • await: Suspending execution for async operations
  • Future: Lazy computations that can be awaited
  • Tokio: Async runtime for network and I/O operations

2. Channels and Communication

  • mpsc: Multi-producer, single-consumer channels
  • unbounded_channel: No limit on queued messages
  • Message Passing: Safe inter-task communication

3. Task Management

  • tokio::spawn: Creating concurrent tasks
  • Background Workers: Long-running async tasks
  • Graceful Shutdown: Coordinated task termination

Error Handling Patterns

1. Error Propagation

  • ? Operator: Automatic error propagation
  • map_err(): Transform error types
  • unwrap_or(): Provide default values
  • Early Return: Exit functions on error conditions

2. Custom Error Types

  • Error Enums: Structured error variants
  • Display Trait: User-friendly error messages
  • Error Trait: Standard error handling interface
  • Nested Errors: Wrapping underlying errors

Functional Programming

1. Iterators

  • Iterator Trait: Lazy sequence processing
  • map(): Transform elements
  • filter(): Select elements
  • collect(): Materialize results
  • fold() and reduce(): Aggregation operations

2. Closures

  • Fn Traits: Different closure types (Fn, FnMut, FnOnce)
  • Capture: Borrowing or moving variables into closures
  • Generic Functions: Accept closures as parameters

Serialization and Data Handling

1. Serde Integration

  • Serialize/Deserialize: Automatic JSON conversion
  • Derive Macros: Code generation for traits
  • Field Attributes: Control serialization behavior
  • Custom Serialization: Manual implementation when needed

2. Data Structures

  • HashMap: Key-value storage with O(1) lookup
  • Vec: Dynamic arrays with growth
  • VecDeque: Double-ended queues
  • BTreeMap: Ordered key-value storage

String and Text Processing

1. String Types

  • String: Owned, mutable UTF-8 text
  • &str: Borrowed string slices
  • String Conversion: .to_string(), .into(), format!()
  • Unicode Support: Full UTF-8 character handling

2. Text Operations

  • Splitting: .split(), .split_whitespace(), .lines()
  • Transformation: .to_uppercase(), .to_lowercase(), .trim()
  • Searching: .contains(), .starts_with(), .find()
  • Formatting: format!() macro with type safety

Database and I/O

1. SQLx Integration

  • Connection Pooling: Efficient database resource management
  • Prepared Statements: SQL injection prevention
  • Async Database: Non-blocking database operations
  • Type Safety: Compile-time SQL type checking

2. File Operations

  • Path Handling: Safe file system navigation
  • Error Handling: Graceful I/O failure handling
  • Async I/O: Non-blocking file operations

Security and Validation

1. Input Validation

  • JSON Schema: Structure validation
  • Type Checking: Compile-time safety
  • Sanitization: Safe string processing
  • Bounds Checking: Array access safety

2. Cryptography

  • Hashing: Password security (SHA-256, recommend bcrypt/Argon2)
  • Random Generation: Secure UUID creation
  • Token Management: Session and authentication tokens

Production Patterns

1. Configuration Management

  • Environment Variables: Runtime configuration
  • Config Files: Structured settings
  • Defaults: Fallback values
  • Validation: Configuration checking

2. Monitoring and Metrics

  • Structured Logging: Tracing integration
  • Performance Metrics: Request timing and counting
  • Health Checks: Service status monitoring
  • Resource Tracking: Memory and CPU usage

3. Testing and Quality

  • Unit Tests: Function-level testing
  • Integration Tests: Component interaction testing
  • Property Testing: Random input validation
  • Benchmarking: Performance measurement

This comprehensive coverage of Rust concepts provides a solid foundation for building robust, safe, and efficient MCP servers. Each concept builds upon the others to create production-ready applications.

Best Practices

Error Handling

  1. Use Custom Error Types: Define specific error types for different failure modes
  2. Propagate Errors Properly: Use ? operator and Result types consistently
  3. Provide Meaningful Messages: Include context in error messages
  4. Log Errors Appropriately: Use structured logging for debugging
#[derive(Debug)]
pub enum McpError {
    ValidationError(String),
    DatabaseError(String),
    AuthenticationError(String),
    InternalError(String),
}

impl std::fmt::Display for McpError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            McpError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
            McpError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
            // ... other variants
        }
    }
}

External Learning Resources

Error Handling Best Practices:

Security

  1. Input Validation: Always validate inputs with proper schemas
  2. Path Safety: Use path canonicalization to prevent directory traversal
  3. Rate Limiting: Implement rate limiting for production deployments
  4. Authentication: Use proper authentication mechanisms
  5. Secure Defaults: Default to secure configurations

External Learning Resources

Security Best Practices:

Cryptography and Authentication:

Performance

  1. Async/Await: Use async programming for I/O operations
  2. Connection Pooling: Pool database and HTTP connections
  3. Caching: Implement appropriate caching strategies
  4. Resource Limits: Set memory and connection limits
  5. Monitoring: Monitor performance metrics

External Learning Resources

Performance Optimization:

Memory Management:

Async Performance:

Code Organization

  1. Modular Design: Separate concerns into different modules
  2. Configuration: Make servers configurable for different environments
  3. Testing: Write comprehensive unit and integration tests
  4. Documentation: Document APIs and configuration options
  5. Logging: Use structured logging throughout

Production Deployment

Configuration Management

Use hierarchical configuration with environment-specific overrides:

// Load configuration in order of priority
pub fn load_config() -> Result<Config, ConfigError> {
    let mut config = Config::default();
    
    // 1. Load from config file
    if let Ok(config_path) = env::var("CONFIG_FILE") {
        config = Config::from_file(&config_path)?;
    }
    
    // 2. Override with environment variables
    config.override_from_env()?;
    
    // 3. Override with command line arguments
    config.override_from_args()?;
    
    Ok(config)
}

Monitoring and Observability

  1. Structured Logging: Use JSON logging for production
  2. Metrics Collection: Collect performance and business metrics
  3. Health Checks: Implement comprehensive health checks
  4. Distributed Tracing: Use tracing for request correlation
  5. Alerting: Set up alerts for critical issues

Deployment Patterns

  1. Containerization: Use Docker for consistent deployments
  2. Service Mesh: Consider service mesh for microservices
  3. Load Balancing: Distribute load across multiple instances
  4. Blue-Green Deployment: Enable zero-downtime deployments
  5. Circuit Breakers: Implement circuit breakers for resilience

Testing Strategy

# Run all tests
cargo test

# Run specific example tests
cargo test --bin example_01_hello_world

# Run integration tests
cargo test --test integration

# Check code quality
cargo clippy
cargo fmt --check

Performance Testing

# Run benchmarks
cargo bench

# Memory usage profiling
cargo run --release --bin example_11_monitoring

# Load testing with external tools
# (use tools like wrk, bombardier, or custom load tests)

Conclusion

This tutorial demonstrates a complete progression from simple MCP servers to production-ready enterprise applications. Each example builds upon previous concepts while introducing new patterns and best practices.

The examples cover:

  • Fundamentals: Basic MCP concepts and server structure
  • Intermediate: Configuration, file operations, database integration
  • Advanced: Monitoring, task queues, authentication, notifications
  • Enterprise: Search, blockchain, ML, microservices, complete applications

For production use, focus on:

  • Security best practices
  • Comprehensive error handling
  • Performance optimization
  • Monitoring and observability
  • Testing and validation

The complete source code for all examples is available in the repository, with each example being a fully functional MCP server that can be run and extended.

External Learning Resources

Production Deployment:

Monitoring and Observability:

Load Testing and Benchmarking:

Additional Resources

Official Documentation

Community Resources

Tools and Development

Contributing

Contributions to improve examples, add new patterns, or enhance documentation are welcome. Please follow Rust best practices and include tests for new functionality.

How to Contribute

  • Fork the repository and create a feature branch
  • Follow the established code style and documentation patterns
  • Add tests for new functionality
  • Update the tutorial documentation if needed
  • Submit a pull request with a clear description of changes