MechCrate Rust CLI Development Guide
This guide covers how to develop, build, and extend the MechCrate CLI (mx) and MCP server.
Related Documentation:
- MX Rust CLI and MCP Server Guide - Comprehensive build and management guide
- MX Quick Reference - Quick reference card for daily development
Architecture Overview
MechCrate uses a Cargo workspace with three crates:
mech-crate/├── Cargo.toml # Workspace manifest├── crates/│ ├── mx-lib/ # Shared library (core logic)│ ├── mx-cli/ # CLI binary (user-facing commands)│ └── mx-mcp-server/ # MCP server (AI agent interface)├── templates/ # Recipe templates and project scaffolding└── bin/ # Legacy bash scripts (being phased out)Crate Responsibilities
| Crate | Purpose | Output |
|---|---|---|
mx-lib | Core business logic, shared across CLI and MCP | Library |
mx-cli | User-facing CLI commands | mx binary |
mx-mcp-server | JSON-RPC server for AI agents | mx-mcp binary |
Prerequisites
- Rust via rustup; the exact toolchain is pinned in
rust-toolchain.tomland installed on the first build - Docker (for testing recipes)
- Make (for project Makefiles)
Building
Quick Build
# Build all crates (debug)cargo build
# Build all crates (release)cargo build --release
# Build specific cratecargo build -p mx-cli --releasecargo build -p mx-mcp-server --releaseOutput Locations
target/debug/mx # Debug CLI binarytarget/debug/mx-mcp # Debug MCP server binarytarget/release/mx # Release CLI binarytarget/release/mx-mcp # Release MCP server binaryDevelopment Build with Watch
# Install cargo-watch if neededcargo install cargo-watch
# Rebuild on changescargo watch -x 'build -p mx-cli'Project Structure
mx-lib (Shared Library)
crates/mx-lib/├── Cargo.toml└── src/ ├── lib.rs # Module exports ├── error.rs # Error types (thiserror) ├── paths.rs # Path resolution (~/.mech-crate) ├── config.rs # Global configuration ├── project.rs # Project detection and analysis ├── recipe/ │ ├── mod.rs # Recipe module exports │ ├── parser.rs # recipe.json parsing │ └── installer.rs # Recipe installation logic ├── template/ │ ├── mod.rs # Template module exports │ └── engine.rs # Tera template engine ├── docker/ │ └── mod.rs # Docker/Compose wrappers ├── infra/ │ ├── mod.rs # Infrastructure module │ └── config.rs # Provider configuration ├── router/ │ └── mod.rs # Traefik router management ├── mcp/ │ └── mod.rs # MCP server config & lifecycle ├── corpus/ │ ├── mod.rs # Techniques corpus (pgvector) exports │ ├── store.rs # CorpusStore: Neon→local fallback, hybrid search │ ├── embed.rs # EmbeddingProvider trait + OpenAI-compatible embedder │ ├── chunk.rs # Heading-aware markdown chunker │ ├── frontmatter.rs # YAML frontmatter parser │ ├── config.rs # RagConfig (rag.toml + env) │ └── ingest.rs # Scan/ingest pipeline ├── upgrade/ │ └── mod.rs # Project upgrade functionality └── unyform/ └── mod.rs # Unyform API clientmx-cli (CLI Binary)
crates/mx-cli/├── Cargo.toml└── src/ ├── main.rs # Entry point, clap setup └── commands/ ├── mod.rs # Command module exports ├── init.rs # mx init ├── new.rs # mx new ├── add.rs # mx add ├── recipes.rs # mx recipes ├── dev.rs # mx dev/up/down/logs/etc ├── build.rs # mx build ├── router.rs # mx router ├── infra.rs # mx infra ├── doctor.rs # mx doctor ├── unyform.rs # mx unyform/login/logout ├── mcp.rs # mx mcp └── upgrade.rs # mx upgrademx-mcp-server (MCP Server)
crates/mx-mcp-server/├── Cargo.toml└── src/ ├── main.rs # Entry point, JSON-RPC server ├── tools/ │ └── mod.rs # Tool definitions and handlers ├── project.rs # Project analysis for AI ├── rag/ │ └── mod.rs # Documentation search └── unyform/ └── mod.rs # Unyform tools for AIAdding a New CLI Command
1. Create the Command Module
Create crates/mx-cli/src/commands/mycommand.rs:
//! `mx mycommand` - Description of what it does
use anyhow::Result;use clap::Args;use console::style;
/// Short description for help text#[derive(Args, Debug)]pub struct MyCommand { /// Argument description #[arg(short, long)] flag: bool,
/// Positional argument name: Option<String>,}
impl MyCommand { pub async fn run(&self) -> Result<()> { println!( "{} Running mycommand...", style("→").cyan().bold() );
// Use mx-lib for core logic // ...
println!( "{} Done!", style("✓").green().bold() );
Ok(()) }}2. Register the Command
In crates/mx-cli/src/commands/mod.rs:
pub mod mycommand; // Add this lineIn crates/mx-cli/src/main.rs:
// Add importuse commands::mycommand::MyCommand;
// Add to Commands enum#[derive(Subcommand)]enum Commands { // ... existing commands ...
/// Short description Mycommand(MyCommand),}
// Add handler in main()let result = match cli.command { // ... existing handlers ... Commands::Mycommand(cmd) => cmd.run().await,};3. Add Subcommands (Optional)
For commands with subcommands like mx recipes list:
use clap::{Args, Subcommand};
#[derive(Args, Debug)]pub struct MyCommand { #[command(subcommand)] command: MySubcommands,}
#[derive(Subcommand, Debug)]enum MySubcommands { /// List items List(ListArgs), /// Show item info Info(InfoArgs),}
#[derive(Args, Debug)]struct ListArgs { #[arg(short, long)] all: bool,}
#[derive(Args, Debug)]struct InfoArgs { name: String,}
impl MyCommand { pub async fn run(&self) -> Result<()> { match &self.command { MySubcommands::List(args) => self.list(args).await, MySubcommands::Info(args) => self.info(args).await, } }
async fn list(&self, args: &ListArgs) -> Result<()> { // ... }
async fn info(&self, args: &InfoArgs) -> Result<()> { // ... }}Adding Shared Logic to mx-lib
1. Create a New Module
Create crates/mx-lib/src/mymodule.rs or crates/mx-lib/src/mymodule/mod.rs:
//! My module description
use crate::error::{Error, Result};
pub struct MyService { // fields}
impl MyService { pub fn new() -> Result<Self> { Ok(Self { // ... }) }
pub fn do_something(&self) -> Result<()> { // ... Ok(()) }}2. Export from lib.rs
In crates/mx-lib/src/lib.rs:
pub mod mymodule;
// Optionally re-export for conveniencepub use mymodule::MyService;3. Use in CLI or MCP Server
use mx_lib::MyService;
let service = MyService::new()?;service.do_something()?;Adding an MCP Tool
1. Define the Tool Handler
In crates/mx-mcp-server/src/tools/mod.rs:
// Add to ToolHandler enumpub enum ToolHandler { // ... existing handlers ... MyTool,}
// Add tool definitionpub fn register_tools(registry: &mut ToolRegistry) { // ... existing tools ...
registry.register(ToolDefinition { name: "my_tool".to_string(), description: "Description for AI agents".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "param1": { "type": "string", "description": "Parameter description" } }, "required": ["param1"] }), handler: ToolHandler::MyTool, });}
// Add execute handlerpub async fn execute_tool( handler: &ToolHandler, params: &serde_json::Value,) -> ToolResult { match handler { // ... existing handlers ... ToolHandler::MyTool => { let param1 = params["param1"].as_str() .ok_or("param1 is required")?;
// Use mx-lib for logic let result = mx_lib::my_module::do_something(param1)?;
Ok(serde_json::json!({ "success": true, "result": result })) } }}Error Handling
Using thiserror in mx-lib
use thiserror::Error;
#[derive(Error, Debug)]pub enum Error { #[error("IO error: {0}")] Io(#[from] std::io::Error),
#[error("JSON error: {0}")] Json(#[from] serde_json::Error),
#[error("Template error: {0}")] Template(#[from] tera::Error),
#[error("Not a MechCrate project")] NotAProject,
#[error("Recipe not found: {0}")] RecipeNotFound(String),
#[error("Configuration error: {0}")] Config(String),
// Add new error variants here}
pub type Result<T> = std::result::Result<T, Error>;Using anyhow in CLI Commands
use anyhow::{Result, Context, bail};
pub async fn run(&self) -> Result<()> { // Add context to errors let file = std::fs::read_to_string(&path) .context("Failed to read config file")?;
// Bail with custom message if !valid { bail!("Invalid configuration: {}", reason); }
Ok(())}Testing
Unit Tests
// In any module#[cfg(test)]mod tests { use super::*;
#[test] fn test_something() { let result = do_something(); assert!(result.is_ok()); }
#[tokio::test] async fn test_async_something() { let result = async_do_something().await; assert!(result.is_ok()); }}Running Tests
# All testscargo test
# Specific cratecargo test -p mx-lib
# Specific testcargo test -p mx-lib test_something
# With outputcargo test -- --nocaptureIntegration Tests
Create crates/mx-cli/tests/integration_test.rs:
use assert_cmd::Command;
#[test]fn test_cli_help() { let mut cmd = Command::cargo_bin("mx").unwrap(); cmd.arg("--help") .assert() .success() .stdout(predicates::str::contains("MechCrate CLI"));}Path Resolution
MechCrate uses a priority-based path resolution system:
use mx_lib::{templates_dir, is_initialized, home_dir};
// Get templates directory (auto-resolved)let templates = templates_dir()?;
// Check if mx init has been runif !is_initialized() { bail!("Run 'mx init' first");}
// Get ~/.mech-crate pathlet home = home_dir()?;Resolution order:
MECH_CRATE_ROOTenv var (development override)~/.mech-crate/templates(standard installation)- Relative to executable (portable mode)
Common Patterns
Interactive Prompts (CLI only)
use dialoguer::{Select, Input, Confirm, MultiSelect};use console::style;
// Single selectionlet options = vec!["Option A", "Option B", "Option C"];let selection = Select::new() .with_prompt("Choose an option") .items(&options) .default(0) .interact()?;
// Text inputlet name: String = Input::new() .with_prompt("Project name") .default("my-project".into()) .interact_text()?;
// Confirmationlet proceed = Confirm::new() .with_prompt("Continue?") .default(true) .interact()?;
// Multiple selectionlet selected = MultiSelect::new() .with_prompt("Select features") .items(&["Feature A", "Feature B", "Feature C"]) .interact()?;Progress Bars
use indicatif::{ProgressBar, ProgressStyle};
let pb = ProgressBar::new(total_items as u64);pb.set_style( ProgressStyle::default_bar() .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}") .unwrap() .progress_chars("█▓░"),);
for item in items { pb.set_message(format!("Processing {}", item.name)); // process item pb.inc(1);}
pb.finish_with_message("Done!");Styled Output
use console::style;
// Colorsprintln!("{}", style("Success!").green());println!("{}", style("Warning!").yellow());println!("{}", style("Error!").red());
// Formattingprintln!("{}", style("Bold text").bold());println!("{}", style("Dim text").dim());println!("{}", style("Underline").underlined());
// Common patternsprintln!("{} Processing...", style("→").cyan().bold());println!("{} Complete!", style("✓").green().bold());println!("{} Failed!", style("✗").red().bold());Dependencies
Key dependencies used across the workspace:
| Dependency | Purpose |
|---|---|
clap | CLI argument parsing |
tokio | Async runtime |
serde / serde_json | Serialization |
anyhow | Error handling (CLI) |
thiserror | Error types (library) |
tracing | Logging |
reqwest | HTTP client |
tera | Template engine |
dialoguer | Interactive prompts |
console | Terminal styling |
indicatif | Progress bars |
walkdir | Directory traversal |
dirs | Platform paths |
Installation Workflow
The CLI uses a two-step workflow:
1. Build
cargo build --release2. Initialize
# Initialize MechCrate (copies templates to ~/.mech-crate/)# Must set MECH_CRATE_ROOT for first-time initMECH_CRATE_ROOT=$(pwd) ./target/release/mx init3. Use Anywhere
# After init, mx works from any directory./target/release/mx recipes list./target/release/mx doctor./target/release/mx new my-projectPath Resolution
The CLI resolves templates automatically:
MECH_CRATE_ROOTenv var (development override)~/.mech-crate/templates(standard installation)- Relative to executable (fallback)
Release Build
# Build optimized release binariescargo build --release
# Strip debug symbols (smaller binary)strip target/release/mxstrip target/release/mx-mcp
# Check binary sizesls -lh target/release/mx target/release/mx-mcpDebugging
Enable Debug Logging
# Set log levelRUST_LOG=debug mx doctorRUST_LOG=mx_lib=trace mx new my-project
# Specific modulesRUST_LOG=mx_lib::recipe=debug mx add api --recipe rust-apiUsing the Verbose Flag
mx -v new my-projectmx --verbose recipes listIDE Setup
VS Code with rust-analyzer
.vscode/settings.json:
{ "rust-analyzer.cargo.features": "all", "rust-analyzer.linkedProjects": [ "./Cargo.toml" ]}Cursor
The workspace is already configured for optimal rust-analyzer support via the workspace Cargo.toml.
Contributing
- Create a feature branch
- Make changes following the patterns above
- Run
cargo fmtandcargo clippy - Add tests for new functionality
- Update documentation if needed
- Submit a PR
# Format codecargo fmt
# Run lintercargo clippy -- -D warnings
# Run all checkscargo fmt && cargo clippy && cargo test