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
reqwestcrate withjsonfeature: Add toCargo.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 viaMCP_AGENT_URLenvironment 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:
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.
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 keyMCP_AGENT_URL(optional): MCP server URL (default:https://mcp.tracemem.com)TRACEMEM_INSTANCE(optional): Instance identifierTRACEMEM_ACTOR(optional): Actor identifier (default:test-read-agent)CUSTOMER_ID(optional): Customer ID to read (default:1003)
Running the Example
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:
// 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 keyMCP_AGENT_URL(optional): MCP server URL (default:https://mcp.tracemem.com)TRACEMEM_INSTANCE(optional): Instance identifierTRACEMEM_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":
// 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 keyMCP_AGENT_URL(optional): MCP server URL (default:https://mcp.tracemem.com)TRACEMEM_INSTANCE(optional): Instance identifierTRACEMEM_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:
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 keyMCP_AGENT_URL(optional): MCP server URL (default:https://mcp.tracemem.com)TRACEMEM_INSTANCE(optional): Instance identifierTRACEMEM_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:
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:
- Initialize MCP Session: Connect to the Agent MCP server
- Create Decision Envelope: Open a decision with appropriate intent
- Perform Operations: Read, write, evaluate policies, etc.
- Close Decision: Commit on success, abort on error
Environment Variable Configuration
All examples use environment variables for configuration:
# 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
- Start with Read Operations: Test read operations first to verify connectivity
- Use Test Data: Use non-production data products for testing
- Check Decision Traces: Review decision traces in the TraceMem dashboard
- Handle Errors Gracefully: Always implement proper error handling and decision cleanup