Zig Agent Examples
This guide provides complete, runnable examples for building TraceMem agents in Zig. Each example demonstrates a specific pattern for interacting with TraceMem's Agent MCP server.
Prerequisites
- Zig 0.12 or higher
- Standard library only (no external dependencies required)
- 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:
const std = @import("std");
const http = std.http;
const json = std.json;
const MCPClient = struct {
base_url: []const u8,
api_key: []const u8,
allocator: std.mem.Allocator,
request_id: std.atomic.Value(u64),
initialized: std.atomic.Value(bool),
const Self = @This();
pub fn init(allocator: std.mem.Allocator, base_url: []const u8, api_key: []const u8) Self {
return Self{
.base_url = base_url,
.api_key = api_key,
.allocator = allocator,
.request_id = std.atomic.Value(u64).init(0),
.initialized = std.atomic.Value(bool).init(false),
};
}
fn nextId(self: *Self) u64 {
return self.request_id.fetchAdd(1, .seq_cst) + 1;
}
pub fn call(self: *Self, method: []const u8, params: ?json.Value) !json.Value {
var request_obj = json.ObjectMap.init(self.allocator);
defer request_obj.deinit();
try request_obj.put("jsonrpc", json.Value{ .string = "2.0" });
try request_obj.put("id", json.Value{ .integer = @intCast(self.nextId()) });
try request_obj.put("method", json.Value{ .string = method });
if (params) |p| {
try request_obj.put("params", p);
} else {
try request_obj.put("params", json.Value{ .object = json.ObjectMap.init(self.allocator) });
}
const request_value = json.Value{ .object = request_obj };
const request_json = try json.stringifyAlloc(self.allocator, request_value, .{});
defer self.allocator.free(request_json);
var uri = try std.Uri.parse(self.base_url);
var client = http.Client{ .allocator = self.allocator };
defer client.deinit();
var headers = http.Headers.init(self.allocator);
defer headers.deinit();
try headers.append("Authorization", try std.fmt.allocPrint(self.allocator, "Agent {s}", .{self.api_key}));
try headers.append("Content-Type", "application/json");
var req = try client.open(.POST, uri, headers, .{});
defer req.deinit();
try req.writer().writeAll(request_json);
try req.finish();
try req.wait();
const response = try req.reader().readAllAlloc(self.allocator, std.math.maxInt(usize));
defer self.allocator.free(response);
var json_parser = json.Parser.init(self.allocator, .alloc_always);
defer json_parser.deinit();
var tree = try json_parser.parse(response);
defer tree.deinit();
const root = tree.root;
if (root.object.get("error")) |error_val| {
const error_obj = error_val.object;
const code = error_obj.get("code").?.integer;
const message = error_obj.get("message").?.string;
return error.MCPError;
}
if (root.object.get("result")) |result| {
return result;
}
return json.Value{ .object = json.ObjectMap.init(self.allocator) };
}
pub fn initialize(self: *Self) !json.Value {
var params_obj = json.ObjectMap.init(self.allocator);
defer params_obj.deinit();
try params_obj.put("protocolVersion", json.Value{ .string = "2024-11-05" });
try params_obj.put("capabilities", json.Value{ .object = json.ObjectMap.init(self.allocator) });
var client_info = json.ObjectMap.init(self.allocator);
defer client_info.deinit();
try client_info.put("name", json.Value{ .string = "tracemem-test-agent" });
try client_info.put("version", json.Value{ .string = "1.0.0" });
try params_obj.put("clientInfo", json.Value{ .object = client_info });
const params = json.Value{ .object = params_obj };
const result = try self.call("initialize", params);
_ = self.initialized.fetchOr(true, .seq_cst);
return result;
}
pub fn listTools(self: *Self) !json.Value {
return self.call("tools/list", null);
}
pub fn callTool(self: *Self, name: []const u8, arguments: json.Value) !json.Value {
if (!self.initialized.load(.seq_cst)) {
_ = try self.initialize();
}
var params_obj = json.ObjectMap.init(self.allocator);
defer params_obj.deinit();
try params_obj.put("name", json.Value{ .string = name });
try params_obj.put("arguments", arguments);
const params = json.Value{ .object = params_obj };
const result = try self.call("tools/call", params);
// Check if the tool result indicates an error
if (result.object.get("isError")) |is_error| {
if (is_error.bool) {
return error.ToolCallFailed;
}
}
// Parse content from tool result
if (result.object.get("content")) |content| {
if (content.array.items.len > 0) {
const first_content = content.array.items[0];
if (first_content.object.get("type")) |content_type| {
if (std.mem.eql(u8, content_type.string, "text")) {
if (first_content.object.get("text")) |text| {
var json_parser = json.Parser.init(self.allocator, .alloc_always);
defer json_parser.deinit();
if (json_parser.parse(text.string)) |parsed| {
defer parsed.deinit();
return parsed.root;
} else |_| {
var result_obj = json.ObjectMap.init(self.allocator);
try result_obj.put("raw_text", text);
return json.Value{ .object = result_obj };
}
}
}
}
}
}
return result;
}
pub fn isInitialized(self: *Self) bool {
return self.initialized.load(.seq_cst);
}
};
const MCPError = error{
MCPError,
ToolCallFailed,
};
Example 1: Read Agent
This example demonstrates reading customer data from a data product.
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Get configuration from environment
const mcp_url = std.process.getEnvVarOwned(allocator, "MCP_AGENT_URL") catch |_|
try allocator.dupe(u8, "https://mcp.tracemem.com");
defer allocator.free(mcp_url);
const api_key = std.process.getEnvVarOwned(allocator, "TRACEMEM_API_KEY") catch |_| {
std.debug.print("ERROR: TRACEMEM_API_KEY environment variable is required\n", .{});
std.process.exit(1);
};
defer allocator.free(api_key);
const instance = std.process.getEnvVarOwned(allocator, "TRACEMEM_INSTANCE") catch |_| null;
defer if (instance) |inst| allocator.free(inst);
const actor = std.process.getEnvVarOwned(allocator, "TRACEMEM_ACTOR") catch |_|
try allocator.dupe(u8, "test-read-agent");
defer allocator.free(actor);
const customer_id = std.process.getEnvVarOwned(allocator, "CUSTOMER_ID") catch |_|
try allocator.dupe(u8, "1003");
defer allocator.free(customer_id);
std.debug.print("{s}\n", .{"=" ** 60});
std.debug.print("TraceMem MCP - Read Test Agent\n", .{});
std.debug.print("{s}\n", .{"=" ** 60});
std.debug.print("\n", .{});
std.debug.print("Connecting to Agent MCP at: {s}\n", .{mcp_url});
if (instance) |inst| {
std.debug.print("Instance: {s}\n", .{inst});
}
std.debug.print("Actor: {s}\n", .{actor});
std.debug.print("Customer ID: {s}\n", .{customer_id});
std.debug.print("\n", .{});
var client = MCPClient.init(allocator, mcp_url, api_key);
var decision_id: ?[]const u8 = null;
defer if (decision_id) |did| allocator.free(did);
// Initialize MCP session
std.debug.print("Initializing MCP session...\n", .{});
const init_result = try client.initialize();
const server_name = if (init_result.object.get("serverInfo")) |si|
(if (si.object.get("name")) |n| n.string else "TraceMem Agent MCP")
else "TraceMem Agent MCP";
std.debug.print("✓ Connected to {s}\n", .{server_name});
std.debug.print("\n", .{});
// Step 1: Create decision envelope
std.debug.print("Step 1: Creating decision envelope...\n", .{});
var decision_args = json.ObjectMap.init(allocator);
defer decision_args.deinit();
try decision_args.put("intent", json.Value{ .string = "test.read.customer" });
try decision_args.put("automation_mode", json.Value{ .string = "autonomous" });
if (instance) |inst| {
try decision_args.put("instance", json.Value{ .string = inst });
}
try decision_args.put("actor", json.Value{ .string = actor });
var metadata = json.ObjectMap.init(allocator);
defer metadata.deinit();
try metadata.put("customer_id", json.Value{ .string = customer_id });
try metadata.put("test_type", json.Value{ .string = "read" });
try decision_args.put("metadata", json.Value{ .object = metadata });
const decision = try client.callTool("decision_create", json.Value{ .object = decision_args });
decision_id = if (decision.object.get("decision_id")) |did|
(if (did.string) |s| try allocator.dupe(u8, s) else null)
else if (decision.object.get("id")) |id|
(if (id.string) |s| try allocator.dupe(u8, s) else null)
else null;
if (decision_id == null) {
return error.FailedToGetDecisionId;
}
std.debug.print("✓ Decision envelope created: {s}\n", .{decision_id.?});
std.debug.print("\n", .{});
// Step 2: Read customer data
std.debug.print("Step 2: Reading customer data...\n", .{});
const customer_id_int = try std.fmt.parseInt(i32, customer_id, 10);
var read_args = json.ObjectMap.init(allocator);
defer read_args.deinit();
try read_args.put("decision_id", json.Value{ .string = decision_id.? });
try read_args.put("product", json.Value{ .string = "planetscale_read_customer_v1" });
try read_args.put("purpose", json.Value{ .string = "web_order" });
var query = json.ObjectMap.init(allocator);
defer query.deinit();
try query.put("id", json.Value{ .integer = customer_id_int });
try read_args.put("query", json.Value{ .object = query });
const read_result = try client.callTool("decision_read", json.Value{ .object = read_args });
std.debug.print("✓ Customer data retrieved\n", .{});
if (read_result.object.get("event_id")) |event_id| {
if (event_id.string) |eid| {
std.debug.print(" Event ID: {s}\n", .{eid});
}
}
if (read_result.object.get("data_ref")) |data_ref| {
if (data_ref.string) |dr| {
std.debug.print(" Data Reference: {s}\n", .{dr});
}
}
if (read_result.object.get("records")) |records| {
if (records.array.items.len > 0) {
std.debug.print(" Records found: {d}\n", .{records.array.items.len});
std.debug.print(" Customer data:\n", .{});
const record_json = try json.stringifyAlloc(allocator, records.array.items[0], .{ .indent = .space, .whitespace = .indent_2 });
defer allocator.free(record_json);
std.debug.print("{s}\n", .{record_json});
} else {
std.debug.print(" No records found\n", .{});
}
} else {
std.debug.print(" No records found\n", .{});
}
std.debug.print("\n", .{});
// Step 3: Close decision (commit)
std.debug.print("Step 3: Committing decision...\n", .{});
var close_args = json.ObjectMap.init(allocator);
defer close_args.deinit();
try close_args.put("decision_id", json.Value{ .string = decision_id.? });
try close_args.put("action", json.Value{ .string = "commit" });
const close_result = try client.callTool("decision_close", json.Value{ .object = close_args });
std.debug.print("✓ Decision committed\n", .{});
if (close_result.object.get("status")) |status| {
if (status.string) |s| {
std.debug.print(" Status: {s}\n", .{s});
}
}
std.debug.print("\n", .{});
// Summary
std.debug.print("{s}\n", .{"=" ** 60});
std.debug.print("Summary\n", .{});
std.debug.print("{s}\n", .{"=" ** 60});
std.debug.print("Decision ID: {s}\n", .{decision_id.?});
std.debug.print("Result: ✓ Read operation completed successfully\n", .{});
std.debug.print("\n", .{});
}
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"
zig run test_read_agent.zig
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 and order insertion.
Example 3: Insert Agent (without Policy)
This example demonstrates inserting an order without policy evaluation. 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".
Example 5: Delete Agent
This example demonstrates deleting a target record. The structure follows the same pattern, with UUID validation and delete operation.
Common Patterns
Error Handling Best Practices
Always ensure decision envelopes are properly closed, even on errors:
var decision_id: ?[]const u8 = null;
defer if (decision_id) |did| allocator.free(did);
const result = blk: {
// Create decision and perform operations
const decision = try client.callTool("decision_create", ...);
decision_id = try allocator.dupe(u8, decision.object.get("decision_id").?.string.?);
// Perform operations...
// Commit on success
_ = try client.callTool("decision_close", ...);
break :blk {};
};
if (result) |_| {} else |err| {
// Always abort on error
if (decision_id) |did| {
_ = client.callTool("decision_close", ...) catch |close_err| {
std.debug.print("Failed to abort decision: {}\n", .{close_err});
};
}
return err;
}
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