Successfully created native binaries for both CLI and MCP server using GraalVM native-image. Both binaries work correctly for their respective use cases:
GRAALVM_HOME environment variable setGRAALVM_HOME=/Library/Java/JavaVirtualMachines/graalvm-25.jdk/Contents/HomeCLI 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)
Malli 0.16.4 - Schema validation library
src/mcp_tasks/schema.cljc
(System/getenv "USE_MALLI")USE_MALLI=truemalli/core when loading tasks (expected behavior)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):
-main) for native CLI binarymcp-tasks.cli/-main after loading namespacesServer Binary (src/mcp_tasks/native_server_init.clj):
-main) for native server binarymcp-tasks.main/-main after loading namespacesmcp-tasks.cli/-mainmcp-tasks.native-init/-main → mcp-tasks.cli/-mainmcp-tasks.main/-mainmcp-tasks.native-server-init/-main → mcp-tasks.main/-main["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]
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
Warning: Malformed EDN at line N: Could not locate malli/core__init.class...
Status: Expected behavior, not a bug
Status: Not required for current implementation
The build succeeded without custom reflection configuration files because:
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
Malli alternatives: For native builds requiring validation, consider:
Binary size optimization: Current 38.3 MB could potentially be reduced with:
--gc=G1 (different garbage collector)-O3 optimization levelThe 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.
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:
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |