Liking cljdoc? Tell your friends :D

GraalVM Native-Image Compatibility Assessment

Summary

Successfully created native binaries for both CLI and MCP server using GraalVM native-image. Both binaries work correctly for their respective use cases:

  • CLI Binary: 38.3 MB, works correctly for all CLI commands
  • Server Binary: Similar size, runs as MCP server using stdio transport

Build Process

Prerequisites

  • GraalVM with native-image installed
  • GRAALVM_HOME environment variable set
  • Example: GRAALVM_HOME=/Library/Java/JavaVirtualMachines/graalvm-25.jdk/Contents/Home

Build Commands

CLI Binary:

# Build CLI uberjar
clj -T:build jar-cli

# Build native binary (requires GRAALVM_HOME)
GRAALVM_HOME=/path/to/graalvm clj -T:build native-cli

Output: target/mcp-tasks-<platform>-<arch> (38.3 MB native executable)

Server Binary:

# Build server uberjar
clj -T:build jar-server

# Build native binary (requires GRAALVM_HOME)
GRAALVM_HOME=/path/to/graalvm clj -T:build native-server

# Or use Babashka task (builds both)
bb build-native-server

Output: target/mcp-tasks-server-<platform>-<arch> (native executable)

Dependency Compatibility

✅ Compatible Dependencies

  1. Clojure 1.12.3 - Fully compatible
  2. Cheshire 5.13.0 (JSON) - Works without additional configuration
  3. Babashka/fs 0.5.22 - Filesystem utilities work correctly
  4. Babashka/cli 0.8.61 - CLI parsing works correctly

⚠️ Conditionally Compatible

Malli 0.16.4 - Schema validation library

  • Status: Disabled for native-image builds (by design)
  • Approach: Already implemented in src/mcp_tasks/schema.cljc
    • Uses reader conditionals to check (System/getenv "USE_MALLI")
    • For BB: Malli enabled when USE_MALLI=true
    • For native-image: Malli disabled (validators are no-ops)
    • For JVM: Malli always enabled
  • Impact: Schema validation is skipped in native binary, but this is acceptable for a CLI tool
  • Warnings: Native binary shows warnings about missing malli/core when loading tasks (expected behavior)

Key Implementation Details

Namespace Loading

Problem: Native-image cannot dynamically load namespaces via requiring-resolve

Solution: Created separate entry points for CLI and server binaries:

CLI Binary (src/mcp_tasks/native_init.clj):

  • Explicitly requires all tool namespaces before compilation
  • Serves as entry point (-main) for native CLI binary
  • Delegates to mcp-tasks.cli/-main after loading namespaces

Server Binary (src/mcp_tasks/native_server_init.clj):

  • Explicitly requires all tool namespaces before compilation
  • Serves as entry point (-main) for native server binary
  • Delegates to mcp-tasks.main/-main after loading namespaces
  • Identical pattern to CLI init, but targets server entry point

Entry Points

  • Regular CLI (BB/JVM): mcp-tasks.cli/-main
  • Native CLI binary: mcp-tasks.native-init/-mainmcp-tasks.cli/-main
  • Regular MCP Server (JVM): mcp-tasks.main/-main
  • Native Server binary: mcp-tasks.native-server-init/-mainmcp-tasks.main/-main

Build Configuration

native-image Flags

["native-image"
 "-jar" jar-file
 "--no-fallback"                              ;; No fallback to JVM
 "-H:+ReportExceptionStackTraces"             ;; Better error messages
 "--initialize-at-build-time"                 ;; Initialize all classes at build time
 "--report-unsupported-elements-at-runtime"   ;; (deprecated but harmless)
 "-o" output-binary]

Build Time

  • Compilation: ~2m 46s on M1 Mac
  • Memory usage: Peak RSS 3.39GB

Testing Results

✅ Verified Working

CLI Binary:

# Help command
./target/mcp-tasks-<platform>-<arch> --help

# List tasks
./target/mcp-tasks-<platform>-<arch> list --status open --format human

# All CLI commands tested and working

Server Binary:

# Start MCP server (stdio transport)
./target/mcp-tasks-server-<platform>-<arch>

# Server starts and accepts MCP protocol messages
# Tested via integration tests with :native-binary metadata
# Smoke tests verify startup on all platforms
# Comprehensive tests validate full MCP protocol on Linux

Known Warnings

Warning: Malformed EDN at line N: Could not locate malli/core__init.class...

Status: Expected behavior, not a bug

  • Appears when loading tasks with Malli schema validation
  • Schema validation gracefully falls back to no-ops
  • Does not affect CLI functionality

Reflection Configuration

Status: Not required for current implementation

The build succeeded without custom reflection configuration files because:

  1. All tool namespaces are eagerly loaded at build time
  2. No dynamic reflection in critical paths
  3. Cheshire JSON serialization doesn't require config for our use case
  4. Malli is disabled, avoiding its reflection needs

Recommendations

  1. Future work: If adding features that require reflection, use native-image-agent:

    java -agentlib:native-image-agent=config-output-dir=resources/META-INF/native-image/org.hugoduncan/mcp-tasks \
         -jar target/mcp-tasks-cli-0.1.96.jar list
    
  2. Malli alternatives: For native builds requiring validation, consider:

    • spec (Clojure's built-in validation)
    • Custom validation functions
    • Enable Malli with reflection config (requires investigation)
  3. Binary size optimization: Current 38.3 MB could potentially be reduced with:

    • --gc=G1 (different garbage collector)
    • -O3 optimization level
    • Removing unused dependencies

MCP Client Configuration

The native server binary can be configured in MCP clients:

Claude Code:

claude mcp add mcp-tasks -- /usr/local/bin/mcp-tasks-server

Claude Desktop:

{
  "mcpServers": {
    "mcp-tasks": {
      "command": "/usr/local/bin/mcp-tasks-server"
    }
  }
}

The server binary uses stdio transport and requires no additional arguments or configuration.

Conclusion

Both native binaries (CLI and server) are production-ready for the current feature set. The Malli warnings are cosmetic and don't affect functionality. No blocking issues identified.

Key Benefits:

  • No JVM or Babashka runtime required
  • Fast startup (< 10ms typical)
  • Standalone distribution
  • Cross-platform support (Linux, macOS, Windows)

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close