Java Agent Examples

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

Prerequisites

  • Java 11 or higher
  • No external dependencies required (uses java.net.http from Java 11+)
  • 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 class that handles JSON-RPC 2.0 communication:

java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;

public class MCPClient {
    private final String baseUrl;
    private final String apiKey;
    private final HttpClient httpClient;
    private final AtomicInteger requestId = new AtomicInteger(0);
    private volatile boolean initialized = false;
    private final ObjectMapper objectMapper = new ObjectMapper();

    public MCPClient(String baseUrl, String apiKey) {
        this.baseUrl = baseUrl;
        this.apiKey = apiKey;
        this.httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();
    }

    private int nextId() {
        return requestId.incrementAndGet();
    }

    private JsonNode call(String method, Map<String, Object> params) throws Exception {
        Map<String, Object> request = Map.of(
            "jsonrpc", "2.0",
            "id", nextId(),
            "method", method,
            "params", params != null ? params : Map.of()
        );

        String requestBody = objectMapper.writeValueAsString(request);

        HttpRequest httpRequest = HttpRequest.newBuilder()
            .uri(URI.create(baseUrl))
            .header("Authorization", "Agent " + apiKey)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(requestBody))
            .build();

        HttpResponse<String> response = httpClient.send(httpRequest, 
            HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() != 200) {
            throw new Exception("HTTP error " + response.statusCode() + ": " + response.body());
        }

        JsonNode jsonResponse = objectMapper.readTree(response.body());

        if (jsonResponse.has("error")) {
            JsonNode error = jsonResponse.get("error");
            int code = error.get("code").asInt();
            String message = error.get("message").asText();
            String errorMsg = String.format("MCP Error %d: %s", code, message);
            if (error.has("data")) {
                errorMsg += " - " + error.get("data").toString();
            }
            throw new Exception(errorMsg);
        }

        return jsonResponse.has("result") ? jsonResponse.get("result") : objectMapper.createObjectNode();
    }

    public JsonNode initialize() throws Exception {
        Map<String, Object> params = Map.of(
            "protocolVersion", "2024-11-05",
            "capabilities", Map.of(),
            "clientInfo", Map.of(
                "name", "tracemem-test-agent",
                "version", "1.0.0"
            )
        );

        JsonNode result = call("initialize", params);
        initialized = true;
        return result;
    }

    public JsonNode listTools() throws Exception {
        return call("tools/list", null);
    }

    public JsonNode callTool(String name, Map<String, Object> arguments) throws Exception {
        if (!initialized) {
            initialize();
        }

        Map<String, Object> params = Map.of(
            "name", name,
            "arguments", arguments
        );

        JsonNode result = call("tools/call", params);

        // Check if the tool result indicates an error
        if (result.has("isError") && result.get("isError").asBoolean()) {
            String errorMessage = "Tool call failed";
            if (result.has("content") && result.get("content").isArray() 
                && result.get("content").size() > 0) {
                JsonNode content = result.get("content").get(0);
                if (content.has("type") && content.get("type").asText().equals("text")
                    && content.has("text")) {
                    errorMessage = content.get("text").asText();
                }
            }
            throw new Exception(errorMessage);
        }

        // Parse content from tool result
        if (result.has("content") && result.get("content").isArray() 
            && result.get("content").size() > 0) {
            JsonNode content = result.get("content").get(0);
            if (content.has("type") && content.get("type").asText().equals("text")
                && content.has("text")) {
                String text = content.get("text").asText();
                try {
                    return objectMapper.readTree(text);
                } catch (Exception e) {
                    return objectMapper.createObjectNode()
                        .put("raw_text", text);
                }
            }
        }

        return result;
    }

    public boolean isInitialized() {
        return initialized;
    }
}

Note: For JSON handling, you can use Jackson (com.fasterxml.jackson.core:jackson-databind) or Gson. The example above uses Jackson.

Example 1: Read Agent

This example demonstrates reading customer data from a data product.

java
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;

public class TestReadAgent {
    public static void main(String[] args) {
        try {
            runReadTest();
        } catch (Exception e) {
            System.err.println("✗ Error: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }

    private static void runReadTest() throws Exception {
        // Get configuration from environment
        String mcpUrl = System.getenv("MCP_AGENT_URL");
        if (mcpUrl == null || mcpUrl.isEmpty()) {
            mcpUrl = "https://mcp.tracemem.com";
        }
        String apiKey = System.getenv("TRACEMEM_API_KEY");
        if (apiKey == null || apiKey.isEmpty()) {
            throw new Exception("ERROR: TRACEMEM_API_KEY environment variable is required");
        }
        String instance = System.getenv("TRACEMEM_INSTANCE");
        String actor = System.getenv("TRACEMEM_ACTOR");
        if (actor == null || actor.isEmpty()) {
            actor = "test-read-agent";
        }
        String customerId = System.getenv("CUSTOMER_ID");
        if (customerId == null || customerId.isEmpty()) {
            customerId = "1003";
        }

        System.out.println("=".repeat(60));
        System.out.println("TraceMem MCP - Read Test Agent");
        System.out.println("=".repeat(60));
        System.out.println();
        System.out.println("Connecting to Agent MCP at: " + mcpUrl);
        if (instance != null && !instance.isEmpty()) {
            System.out.println("Instance: " + instance);
        }
        System.out.println("Actor: " + actor);
        System.out.println("Customer ID: " + customerId);
        System.out.println();

        MCPClient client = new MCPClient(mcpUrl, apiKey);
        String decisionId = null;

        try {
            // Initialize MCP session
            System.out.println("Initializing MCP session...");
            JsonNode initResult = client.initialize();
            String serverName = "TraceMem Agent MCP";
            if (initResult.has("serverInfo") && initResult.get("serverInfo").has("name")) {
                serverName = initResult.get("serverInfo").get("name").asText();
            }
            System.out.println("✓ Connected to " + serverName);
            System.out.println();

            // Step 1: Create decision envelope
            System.out.println("Step 1: Creating decision envelope...");
            Map<String, Object> decisionArgs = Map.of(
                "intent", "test.read.customer",
                "automation_mode", "autonomous",
                "instance", instance != null ? instance : "",
                "actor", actor,
                "metadata", Map.of(
                    "customer_id", customerId,
                    "test_type", "read"
                )
            );

            JsonNode decision = client.callTool("decision_create", decisionArgs);
            decisionId = decision.has("decision_id") 
                ? decision.get("decision_id").asText()
                : (decision.has("id") ? decision.get("id").asText() : null);

            if (decisionId == null || decisionId.isEmpty()) {
                throw new Exception("Failed to get decision_id from decision_create response");
            }
            System.out.println("✓ Decision envelope created: " + decisionId);
            System.out.println();

            // Step 2: Read customer data
            System.out.println("Step 2: Reading customer data...");
            int customerIdInt = Integer.parseInt(customerId);
            Map<String, Object> readArgs = Map.of(
                "decision_id", decisionId,
                "product", "planetscale_read_customer_v1",
                "purpose", "web_order",
                "query", Map.of("id", customerIdInt)
            );

            JsonNode readResult = client.callTool("decision_read", readArgs);

            System.out.println("✓ Customer data retrieved");
            if (readResult.has("event_id")) {
                System.out.println("  Event ID: " + readResult.get("event_id").asText());
            }
            if (readResult.has("data_ref")) {
                System.out.println("  Data Reference: " + readResult.get("data_ref").asText());
            }
            if (readResult.has("records") && readResult.get("records").isArray()) {
                JsonNode records = readResult.get("records");
                if (records.size() > 0) {
                    System.out.println("  Records found: " + records.size());
                    System.out.println("  Customer data:");
                    System.out.println(records.get(0).toPrettyString());
                } else {
                    System.out.println("  No records found");
                }
            } else {
                System.out.println("  No records found");
            }
            System.out.println();

            // Step 3: Close decision (commit)
            System.out.println("Step 3: Committing decision...");
            Map<String, Object> closeArgs = Map.of(
                "decision_id", decisionId,
                "action", "commit"
            );

            JsonNode closeResult = client.callTool("decision_close", closeArgs);
            System.out.println("✓ Decision committed");
            if (closeResult.has("status")) {
                System.out.println("  Status: " + closeResult.get("status").asText());
            }
            System.out.println();

            // Summary
            System.out.println("=".repeat(60));
            System.out.println("Summary");
            System.out.println("=".repeat(60));
            System.out.println("Decision ID: " + decisionId);
            System.out.println("Result: ✓ Read operation completed successfully");
            System.out.println();

        } catch (Exception error) {
            System.err.println("✗ Error: " + error.getMessage());
            error.printStackTrace();

            // Try to close decision on error
            if (decisionId != null) {
                try {
                    System.out.println("Attempting to abort decision...");
                    client.callTool("decision_close", Map.of(
                        "decision_id", decisionId,
                        "action", "abort",
                        "reason", "Error occurred: " + error.getMessage()
                    ));
                    System.out.println("✓ Decision aborted");
                } catch (Exception closeError) {
                    System.err.println("Failed to abort decision: " + closeError.getMessage());
                }
            }
            throw error;
        }
    }
}

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"
javac -cp ".:jackson-databind-2.15.0.jar" TestReadAgent.java
java -cp ".:jackson-databind-2.15.0.jar" TestReadAgent

Example 2: Insert Agent (with Policy)

This example demonstrates inserting an order with policy evaluation.

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

// Step 2: Evaluate policy
System.out.println("Step 2: Evaluating discount_cap_v1 policy...");
Map<String, Object> policyArgs = Map.of(
    "decision_id", decisionId,
    "policy_id", "discount_cap_v1",
    "inputs", Map.of("proposed_discount", proposedDiscount)
);

JsonNode policyResult = client.callTool("decision_evaluate", policyArgs);

String outcome = policyResult.has("outcome") 
    ? policyResult.get("outcome").asText() 
    : "unknown";
System.out.println("✓ Policy evaluation completed");
System.out.println("  Policy ID: discount_cap_v1");
System.out.println("  Proposed Discount: " + proposedDiscount);
System.out.println("  Outcome: " + outcome);
// ... (handle policy outcomes)

// Step 3: Insert order
Map<String, Object> writeArgs = Map.of(
    "decision_id", decisionId,
    "product", "planetscale_insert_order_v1",
    "purpose", "web_order",
    "mutation", Map.of(
        "operation", "insert",
        "records", List.of(Map.of(
            "customer_id", customerId,
            "product_id", productId,
            "quantity", quantity,
            "total_amount", totalAmount,
            "order_status", orderStatus
        ))
    )
);

JsonNode writeResult = client.callTool("decision_write", writeArgs);

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.

java
// Step 2: Update product stock
Map<String, Object> writeArgs = Map.of(
    "decision_id", decisionId,
    "product", "planetscale_update_product_stock_v1",
    "purpose", "web_order",
    "mutation", Map.of(
        "operation", "update",
        "records", List.of(Map.of(
            "product_id", productId,  // Key field for identification
            "stock_quantity", stockQuantity  // Field to update
        ))
    )
);

JsonNode writeResult = client.callTool("decision_write", writeArgs);

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.

java
import java.util.regex.Pattern;

// UUID validation
Pattern uuidPattern = Pattern.compile(
    "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
    Pattern.CASE_INSENSITIVE
);
if (!uuidPattern.matcher(targetId).matches()) {
    throw new Exception("ERROR: TARGET_ID must be a valid UUID format. Received: " + targetId);
}

// Step 2: Delete target record
Map<String, Object> writeArgs = Map.of(
    "decision_id", decisionId,
    "product", "planetscale_delete_target_v1",
    "purpose", "delete_target",
    "mutation", Map.of(
        "operation", "delete",
        "records", List.of(Map.of("id", targetId))  // Key field for deletion
    )
);

JsonNode writeResult = client.callTool("decision_write", writeArgs);

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:

java
String decisionId = null;
try {
    // Create decision and perform operations
    JsonNode decision = client.callTool("decision_create", ...);
    decisionId = decision.get("decision_id").asText();
    
    // Perform operations...
    
    // Commit on success
    client.callTool("decision_close", Map.of(
        "decision_id", decisionId,
        "action", "commit"
    ));
} catch (Exception e) {
    // Always abort on error
    if (decisionId != null) {
        try {
            client.callTool("decision_close", Map.of(
                "decision_id", decisionId,
                "action", "abort",
                "reason", "Error occurred: " + e.getMessage()
            ));
        } catch (Exception closeError) {
            // Log but don't fail on abort failure
            System.err.println("Failed to abort decision: " + closeError.getMessage());
        }
    }
    throw e;
}

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