Rust Agent Examples

This guide provides complete, runnable examples for building TraceMem agents in Rust. Each example demonstrates a specific pattern for interacting with TraceMem's Agent MCP server.

Prerequisites

  • Rust 1.70 or higher
  • reqwest crate with json feature: Add to Cargo.toml:
    toml
    [dependencies]
    reqwest = { version = "0.11", features = ["json"] }
    tokio = { version = "1", features = ["full"] }
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"
    
  • Access to a TraceMem Agent MCP server (default: https://mcp.tracemem.com)
  • A valid TraceMem API key

Connection Details

  • Endpoint: https://mcp.tracemem.com (or set via MCP_AGENT_URL environment variable)
  • Protocol: JSON-RPC 2.0 over HTTP
  • Authentication: Authorization: Agent <your-api-key> header

MCP Client Implementation

First, let's create a reusable MCP client that handles JSON-RPC 2.0 communication:

rust
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;

#[derive(Debug, Serialize, Deserialize)]
struct JsonRpcRequest {
    jsonrpc: String,
    id: u64,
    method: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    params: Option<Value>,
}

#[derive(Debug, Serialize, Deserialize)]
struct JsonRpcResponse {
    jsonrpc: String,
    id: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    result: Option<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<JsonRpcError>,
}

#[derive(Debug, Serialize, Deserialize)]
struct JsonRpcError {
    code: i32,
    message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    data: Option<Value>,
}

#[derive(Debug, Serialize, Deserialize)]
struct ToolContent {
    #[serde(rename = "type")]
    content_type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    text: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
struct ToolCallResult {
    content: Vec<ToolContent>,
    #[serde(skip_serializing_if = "Option::is_none")]
    is_error: Option<bool>,
}

pub struct MCPClient {
    base_url: String,
    api_key: String,
    client: Client,
    request_id: std::sync::atomic::AtomicU64,
    initialized: std::sync::atomic::AtomicBool,
}

impl MCPClient {
    pub fn new(base_url: String, api_key: String) -> Self {
        MCPClient {
            base_url,
            api_key,
            client: Client::new(),
            request_id: std::sync::atomic::AtomicU64::new(0),
            initialized: std::sync::atomic::AtomicBool::new(false),
        }
    }

    fn next_id(&self) -> u64 {
        self.request_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1
    }

    async fn call(&self, method: &str, params: Option<Value>) -> Result<Value, Box<dyn std::error::Error>> {
        let request = JsonRpcRequest {
            jsonrpc: "2.0".to_string(),
            id: self.next_id(),
            method: method.to_string(),
            params,
        };

        let mut req = self.client
            .post(&self.base_url)
            .header("Authorization", format!("Agent {}", self.api_key))
            .header("Content-Type", "application/json")
            .json(&request);

        let response = req.send().await?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await?;
            return Err(format!("HTTP error {}: {}", status, body).into());
        }

        let json_resp: JsonRpcResponse = response.json().await?;

        if let Some(error) = json_resp.error {
            let mut error_msg = format!("MCP Error {}: {}", error.code, error.message);
            if let Some(data) = error.data {
                error_msg.push_str(&format!(" - {}", serde_json::to_string(&data)?));
            }
            return Err(error_msg.into());
        }

        Ok(json_resp.result.unwrap_or(json!({})))
    }

    pub async fn initialize(&self) -> Result<Value, Box<dyn std::error::Error>> {
        let params = json!({
            "protocolVersion": "2024-11-05",
            "capabilities": {},
            "clientInfo": {
                "name": "tracemem-test-agent",
                "version": "1.0.0"
            }
        });

        let result = self.call("initialize", Some(params)).await?;
        self.initialized.store(true, std::sync::atomic::Ordering::SeqCst);
        Ok(result)
    }

    pub async fn list_tools(&self) -> Result<Value, Box<dyn std::error::Error>> {
        self.call("tools/list", None).await
    }

    pub async fn call_tool(&self, name: &str, arguments: Value) -> Result<Value, Box<dyn std::error::Error>> {
        if !self.initialized.load(std::sync::atomic::Ordering::SeqCst) {
            self.initialize().await?;
        }

        let params = json!({
            "name": name,
            "arguments": arguments
        });

        let result = self.call("tools/call", Some(params)).await?;

        // Check if the tool result indicates an error
        if let Some(tool_result) = result.as_object() {
            if let Some(is_error) = tool_result.get("isError") {
                if is_error.as_bool().unwrap_or(false) {
                    let error_message = if let Some(content) = tool_result.get("content") {
                        if let Some(content_array) = content.as_array() {
                            if let Some(first) = content_array.first() {
                                if let Some(content_obj) = first.as_object() {
                                    if let Some(text) = content_obj.get("text") {
                                        text.as_str().unwrap_or("Tool call failed").to_string()
                                    } else {
                                        "Tool call failed".to_string()
                                    }
                                } else {
                                    "Tool call failed".to_string()
                                }
                            } else {
                                "Tool call failed".to_string()
                            }
                        } else {
                            "Tool call failed".to_string()
                        }
                    } else {
                        "Tool call failed".to_string()
                    };
                    return Err(error_message.into());
                }
            }

            // Parse content from tool result
            if let Some(content) = tool_result.get("content") {
                if let Some(content_array) = content.as_array() {
                    if let Some(first) = content_array.first() {
                        if let Some(content_obj) = first.as_object() {
                            if let Some(text) = content_obj.get("text") {
                                if let Some(text_str) = text.as_str() {
                                    if let Ok(parsed) = serde_json::from_str::<Value>(text_str) {
                                        return Ok(parsed);
                                    }
                                    return Ok(json!({ "raw_text": text_str }));
                                }
                            }
                        }
                    }
                }
            }
        }

        Ok(result)
    }

    pub fn is_initialized(&self) -> bool {
        self.initialized.load(std::sync::atomic::Ordering::SeqCst)
    }
}

Example 1: Read Agent

This example demonstrates reading customer data from a data product.

rust
use std::env;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Get configuration from environment
    let mcp_url = env::var("MCP_AGENT_URL").unwrap_or_else(|_| "https://mcp.tracemem.com".to_string());
    let api_key = env::var("TRACEMEM_API_KEY")
        .map_err(|_| "ERROR: TRACEMEM_API_KEY environment variable is required")?;
    let instance = env::var("TRACEMEM_INSTANCE").ok();
    let actor = env::var("TRACEMEM_ACTOR").unwrap_or_else(|_| "test-read-agent".to_string());
    let customer_id = env::var("CUSTOMER_ID").unwrap_or_else(|_| "1003".to_string());

    println!("{}", "=".repeat(60));
    println!("TraceMem MCP - Read Test Agent");
    println!("{}", "=".repeat(60));
    println!();
    println!("Connecting to Agent MCP at: {}", mcp_url);
    if let Some(ref inst) = instance {
        println!("Instance: {}", inst);
    }
    println!("Actor: {}", actor);
    println!("Customer ID: {}", customer_id);
    println!();

    let client = MCPClient::new(mcp_url, api_key);
    let mut decision_id: Option<String> = None;

    // Initialize MCP session
    println!("Initializing MCP session...");
    let init_result = client.initialize().await?;
    let server_name = init_result
        .get("serverInfo")
        .and_then(|si| si.get("name"))
        .and_then(|n| n.as_str())
        .unwrap_or("TraceMem Agent MCP");
    println!("✓ Connected to {}", server_name);
    println!();

    // Step 1: Create decision envelope
    println!("Step 1: Creating decision envelope...");
    let mut decision_args = serde_json::Map::new();
    decision_args.insert("intent".to_string(), json!("test.read.customer"));
    decision_args.insert("automation_mode".to_string(), json!("autonomous"));
    if let Some(ref inst) = instance {
        decision_args.insert("instance".to_string(), json!(inst));
    }
    decision_args.insert("actor".to_string(), json!(actor));
    let mut metadata = serde_json::Map::new();
    metadata.insert("customer_id".to_string(), json!(customer_id));
    metadata.insert("test_type".to_string(), json!("read"));
    decision_args.insert("metadata".to_string(), json!(metadata));

    let decision = client.call_tool("decision_create", json!(decision_args)).await?;
    decision_id = decision
        .get("decision_id")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .or_else(|| {
            decision
                .get("id")
                .and_then(|v| v.as_str())
                .map(|s| s.to_string())
        });

    let decision_id = decision_id.ok_or("Failed to get decision_id from decision_create response")?;
    println!("✓ Decision envelope created: {}", decision_id);
    println!();

    // Step 2: Read customer data
    println!("Step 2: Reading customer data...");
    let customer_id_int: i32 = customer_id.parse()?;
    let mut read_args = serde_json::Map::new();
    read_args.insert("decision_id".to_string(), json!(decision_id));
    read_args.insert("product".to_string(), json!("planetscale_read_customer_v1"));
    read_args.insert("purpose".to_string(), json!("web_order"));
    let mut query = serde_json::Map::new();
    query.insert("id".to_string(), json!(customer_id_int));
    read_args.insert("query".to_string(), json!(query));

    let read_result = client.call_tool("decision_read", json!(read_args)).await?;

    println!("✓ Customer data retrieved");
    if let Some(event_id) = read_result.get("event_id").and_then(|v| v.as_str()) {
        println!("  Event ID: {}", event_id);
    }
    if let Some(data_ref) = read_result.get("data_ref").and_then(|v| v.as_str()) {
        println!("  Data Reference: {}", data_ref);
    }
    if let Some(records) = read_result.get("records").and_then(|v| v.as_array()) {
        if !records.is_empty() {
            println!("  Records found: {}", records.len());
            println!("  Customer data:");
            println!("{}", serde_json::to_string_pretty(&records[0])?);
        } else {
            println!("  No records found");
        }
    } else {
        println!("  No records found");
    }
    println!();

    // Step 3: Close decision (commit)
    println!("Step 3: Committing decision...");
    let mut close_args = serde_json::Map::new();
    close_args.insert("decision_id".to_string(), json!(decision_id));
    close_args.insert("action".to_string(), json!("commit"));

    let close_result = client.call_tool("decision_close", json!(close_args)).await?;
    println!("✓ Decision committed");
    if let Some(status) = close_result.get("status").and_then(|v| v.as_str()) {
        println!("  Status: {}", status);
    }
    println!();

    // Summary
    println!("{}", "=".repeat(60));
    println!("Summary");
    println!("{}", "=".repeat(60));
    println!("Decision ID: {}", decision_id);
    println!("Result: ✓ Read operation completed successfully");
    println!();

    Ok(())
}

Environment Variables

  • TRACEMEM_API_KEY (required): Your TraceMem agent API key
  • MCP_AGENT_URL (optional): MCP server URL (default: https://mcp.tracemem.com)
  • TRACEMEM_INSTANCE (optional): Instance identifier
  • TRACEMEM_ACTOR (optional): Actor identifier (default: test-read-agent)
  • CUSTOMER_ID (optional): Customer ID to read (default: 1003)

Running the Example

bash
export TRACEMEM_API_KEY="your-api-key"
export CUSTOMER_ID="1"
cargo run --bin test_read_agent

Example 2: Insert Agent (with Policy)

This example demonstrates inserting an order with policy evaluation. The structure follows the same pattern as the read example, with additional steps for policy evaluation:

rust
// Similar structure to read example, with these key additions:

// Step 2: Evaluate policy
println!("Step 2: Evaluating discount_cap_v1 policy...");
let mut policy_args = serde_json::Map::new();
policy_args.insert("decision_id".to_string(), json!(decision_id));
policy_args.insert("policy_id".to_string(), json!("discount_cap_v1"));
let mut inputs = serde_json::Map::new();
inputs.insert("proposed_discount".to_string(), json!(proposed_discount));
policy_args.insert("inputs".to_string(), json!(inputs));

let policy_result = client.call_tool("decision_evaluate", json!(policy_args)).await?;

let outcome = policy_result
    .get("outcome")
    .and_then(|v| v.as_str())
    .unwrap_or("unknown");
println!("✓ Policy evaluation completed");
println!("  Policy ID: discount_cap_v1");
println!("  Proposed Discount: {}", proposed_discount);
println!("  Outcome: {}", outcome);
// ... (handle policy outcomes)

// Step 3: Insert order
let mut write_args = serde_json::Map::new();
write_args.insert("decision_id".to_string(), json!(decision_id));
write_args.insert("product".to_string(), json!("planetscale_insert_order_v1"));
write_args.insert("purpose".to_string(), json!("web_order"));
let mut mutation = serde_json::Map::new();
mutation.insert("operation".to_string(), json!("insert"));
let mut record = serde_json::Map::new();
record.insert("customer_id".to_string(), json!(customer_id));
record.insert("product_id".to_string(), json!(product_id));
record.insert("quantity".to_string(), json!(quantity));
record.insert("total_amount".to_string(), json!(total_amount));
record.insert("order_status".to_string(), json!(order_status));
mutation.insert("records".to_string(), json!(vec![record]));
write_args.insert("mutation".to_string(), json!(mutation));

let write_result = client.call_tool("decision_write", json!(write_args)).await?;

Environment Variables

  • TRACEMEM_API_KEY (required): Your TraceMem agent API key
  • MCP_AGENT_URL (optional): MCP server URL (default: https://mcp.tracemem.com)
  • TRACEMEM_INSTANCE (optional): Instance identifier
  • TRACEMEM_ACTOR (optional): Actor identifier (default: test-insert-agent)
  • CUSTOMER_ID (optional): Customer ID for order (default: 1001)
  • PRODUCT_ID (optional): Product ID for order (default: 2)
  • QUANTITY (optional): Order quantity (default: 1)
  • TOTAL_AMOUNT (optional): Order total amount (default: 99.99)
  • ORDER_STATUS (optional): Order status (default: pending)
  • PROPOSED_DISCOUNT (optional): Proposed discount for policy evaluation (default: 0)

Example 3: Insert Agent (without Policy)

This example demonstrates inserting an order without policy evaluation. The code structure is similar to Example 2, but without the policy evaluation step. Use planetscale_insert_order_no_policy_v1 as the product.

Example 4: Update Agent

This example demonstrates updating product stock. The structure follows the same pattern, with the mutation operation set to "update":

rust
// Step 2: Update product stock
let mut write_args = serde_json::Map::new();
write_args.insert("decision_id".to_string(), json!(decision_id));
write_args.insert("product".to_string(), json!("planetscale_update_product_stock_v1"));
write_args.insert("purpose".to_string(), json!("web_order"));
let mut mutation = serde_json::Map::new();
mutation.insert("operation".to_string(), json!("update"));
let mut record = serde_json::Map::new();
record.insert("product_id".to_string(), json!(product_id));  // Key field
record.insert("stock_quantity".to_string(), json!(stock_quantity));  // Field to update
mutation.insert("records".to_string(), json!(vec![record]));
write_args.insert("mutation".to_string(), json!(mutation));

let write_result = client.call_tool("decision_write", json!(write_args)).await?;

Environment Variables

  • TRACEMEM_API_KEY (required): Your TraceMem agent API key
  • MCP_AGENT_URL (optional): MCP server URL (default: https://mcp.tracemem.com)
  • TRACEMEM_INSTANCE (optional): Instance identifier
  • TRACEMEM_ACTOR (optional): Actor identifier (default: test-update-agent)
  • PRODUCT_ID (optional): Product ID to update (default: 4)
  • STOCK_QUANTITY (optional): New stock quantity (default: 90)

Example 5: Delete Agent

This example demonstrates deleting a target record. The structure follows the same pattern, with UUID validation and delete operation:

rust
use regex::Regex;

// UUID validation
let uuid_regex = Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")?;
if !uuid_regex.is_match(&target_id) {
    return Err(format!("ERROR: TARGET_ID must be a valid UUID format. Received: {}", target_id).into());
}

// Step 2: Delete target record
let mut write_args = serde_json::Map::new();
write_args.insert("decision_id".to_string(), json!(decision_id));
write_args.insert("product".to_string(), json!("planetscale_delete_target_v1"));
write_args.insert("purpose".to_string(), json!("delete_target"));
let mut mutation = serde_json::Map::new();
mutation.insert("operation".to_string(), json!("delete"));
let mut record = serde_json::Map::new();
record.insert("id".to_string(), json!(target_id));  // Key field for deletion
mutation.insert("records".to_string(), json!(vec![record]));
write_args.insert("mutation".to_string(), json!(mutation));

let write_result = client.call_tool("decision_write", json!(write_args)).await?;

Environment Variables

  • TRACEMEM_API_KEY (required): Your TraceMem agent API key
  • MCP_AGENT_URL (optional): MCP server URL (default: https://mcp.tracemem.com)
  • TRACEMEM_INSTANCE (optional): Instance identifier
  • TRACEMEM_ACTOR (optional): Actor identifier (default: test-delete-agent)
  • TARGET_ID (required): UUID of target record to delete

Common Patterns

Error Handling Best Practices

Always ensure decision envelopes are properly closed, even on errors:

rust
let mut decision_id: Option<String> = None;

// Use a result type and ensure cleanup
let result = (|| async {
    // Create decision and perform operations
    let decision = client.call_tool("decision_create", ...).await?;
    decision_id = Some(decision["decision_id"].as_str().unwrap().to_string());
    
    // Perform operations...
    
    // Commit on success
    client.call_tool("decision_close", json!({
        "decision_id": decision_id.as_ref().unwrap(),
        "action": "commit"
    })).await?;
    
    Ok::<(), Box<dyn std::error::Error>>(())
})().await;

// Always abort on error
if let Err(ref e) = result {
    if let Some(ref did) = decision_id {
        let _ = client.call_tool("decision_close", json!({
            "decision_id": did,
            "action": "abort",
            "reason": format!("Error occurred: {}", e)
        })).await;
    }
}

result?;

Decision Envelope Lifecycle

Every agent operation should follow this pattern:

  1. Initialize MCP Session: Connect to the Agent MCP server
  2. Create Decision Envelope: Open a decision with appropriate intent
  3. Perform Operations: Read, write, evaluate policies, etc.
  4. Close Decision: Commit on success, abort on error

Environment Variable Configuration

All examples use environment variables for configuration:

bash
# Required
export TRACEMEM_API_KEY="your-api-key"

# Optional
export MCP_AGENT_URL="https://mcp.tracemem.com"
export TRACEMEM_INSTANCE="my-instance"
export TRACEMEM_ACTOR="my-agent"

Testing Tips

  1. Start with Read Operations: Test read operations first to verify connectivity
  2. Use Test Data: Use non-production data products for testing
  3. Check Decision Traces: Review decision traces in the TraceMem dashboard
  4. Handle Errors Gracefully: Always implement proper error handling and decision cleanup

TraceMem is trace-native infrastructure for AI agents