2
0
Fork
You've already forked LabCmd2
0
unified cmd/journaling architecture
  • C++ 93%
  • CMake 3.5%
  • C 1.3%
  • Python 1.2%
  • Zig 1%
2026年06月29日 18:03:18 -07:00
bindings Add python and zig bindings, embedded endpoints, examples 2026年05月31日 19:05:34 -07:00
examples/rpn Add python and zig bindings, embedded endpoints, examples 2026年05月31日 19:05:34 -07:00
include Add python and zig bindings, embedded endpoints, examples 2026年05月31日 19:05:34 -07:00
src Add python and zig bindings, embedded endpoints, examples 2026年05月31日 19:05:34 -07:00
tests Add python and zig bindings, embedded endpoints, examples 2026年05月31日 19:05:34 -07:00
.gitignore Add python and zig bindings, embedded endpoints, examples 2026年05月31日 19:05:34 -07:00
CMakeLists.txt Add LabCmd2::LabCmd2 ALIAS for in-tree consumers, this enables downstream users to use LabCmd2::LabCmd2 uniformly across all of (find_package, add_subdirectory, FetchContent). 2026年06月29日 18:03:18 -07:00
README.md Add command line tab completion and better REST error reporting 2026年05月29日 14:16:09 -07:00

LabCmd2

A metadata-driven RPC framework in C++17. Define a command once — expose it everywhere (JSON-RPC, MCP, HTTP REST, CLI). With optional undo/redo via transaction journaling.

LabCmd2 fuses the strengths of three predecessor projects:

  • LabCmd — C++17 FFI, JSON-RPC 2.0, lock-free queues
  • inception-mcp — MCP stdio transport, modular tool registry, Python ecosystem
  • db9-cli — metadata-driven domain/subcommand dispatch over triple-store domains
┌─────────────────────────────────────────────────────────────────┐
│ Clients │
│ JSON-RPC │ MCP │ HTTP REST │ CLI │ Python (future) │
└────────────┴───────┴─────────────┴───────┴──────────────────────┘
 │
 ▼
┌─────────────────────────────────────────────────────────────────┐
│ Transport Layer │
│ │
│ StdioJSONRpcServer MCPStdioServer RestServer │
│ (stdio, JSON-RPC) (stdio, MCP protocol) (TCP, HTTP/1.1) │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ CommandRegistry (singleton) │ │
│ │ │ │
│ │ domain:subcommand ──▶ flat map lookup ──▶ handler │ │
│ │ │ │
│ │ Also exposes: │ │
│ │ • CLI help text (auto-generated) │ │
│ │ • MCP tools/list JSON (auto-generated) │ │
│ │ • HTTP REST routes (auto-generated) │ │
│ │ • JSON Schema (auto-generated) │ │
│ │ • Transaction Journal (opt-in undo/redo) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ CommandJournal │ │
│ │ │ │
│ │ Worker thread (async exec) ──▶ done queue │ │
│ │ ProcessPending() ──▶ commit ──▶ journal chain │ │
│ │ Undo / Redo / History / Clear │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Quick Start

# Build
mkdir build && cd build
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DLABCMD_BUILD_TESTS=ON
ninja
# JSON-RPC over stdio (one-shot)
echo '{"jsonrpc":"2.0","method":"system:ping","id":1}' | ./bin/labcmd2-daemon
# → {"id":1,"jsonrpc":"2.0","result":"{\"payload\":[\"pong\"]}"}
# MCP mode (stdio)
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024年11月05日","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' | \
 ./bin/labcmd2-daemon --mcp
# HTTP REST server
./bin/labcmd2-daemon --http 2323 &
curl http://localhost:2323/api/v1/health
# → {"status":"ok"}
# CLI (persistent daemon via FIFOs)
./bin/labcmd2-cli start
./bin/labcmd2-cli call system:ping
./bin/labcmd2-cli call test:add --json '{"a":3,"b":4}'
./bin/labcmd2-cli stop

Core Concepts

Single Source of Truth

Every command is defined as a SubcommandDescriptor containing: name, handler, description, parameters, CLI usage, HTTP method/path, MCP name. One definition drives all transports.

SubcommandDescriptor sub;
sub.name = "add";
sub.full_command_name = "test:add";
sub.handler = [](const std::vector<Argument> &args) -> Ack { ... };
sub.description = "Add two integers";
sub.parameters = { /* ParameterDescriptor... */ };
sub.http_method = "POST";
sub.mcp_name = "test_add";

Domains and Subcommands

Commands are organized into domains (e.g. system, test, rope, triple) each containing multiple subcommands. Dispatch uses the flat key domain:subcommand:

Key Transport Example
system:ping JSON-RPC {"method":"system:ping","id":1}
system_ping MCP tools/call with name: "system_ping"
/api/v1/system/ping HTTP REST GET /api/v1/system/ping
system:ping CLI labcmd2-cli call system:ping

Argument and Ack

// Argument: tagged union carrying a single value
enum SemanticType { void_st, bool_st, int_st, float_st, string_st, array_st, object_st };
struct Argument {
 SemanticType type;
 union { bool boolArg; int32_t intArg; double floatArg; std::string stringArg; ... };
};
// Ack: command response
struct Ack {
 std::string path; // response path / error routing
 std::vector<Argument> payload; // result values
};

Transaction Journaling (opt-in)

Commands can opt into undo/redo by setting journalable = true and providing an undo closure:

SubcommandDescriptor sub;
sub.name = "add";
sub.full_command_name = "test:add";
sub.journalable = true;
sub.undo = [](const std::vector<Argument> &args) {
 // reverse the side effects of the handler
};
sub.handler = [](const std::vector<Argument> &args) -> Ack { ... };

Two-phase model (adapted from MeshulaLab's Transaction/JournalProvider):

Phase Thread Purpose Replayed?
exec Worker thread Heavy/async work (I/O, computation) Never
commit Calling thread Fast deterministic state mutation Yes (on redo)
undo Calling thread Reverses commit Yes (on undo)

For synchronous commands (no exec), the handler runs as commit directly on the calling thread.

Built-in journal commands (always available, non-journaled):

Command Description
system:undo Undo the last journaled command
system:redo Redo the last undone command
system:history List all journal entries with current position
system:journal-clear Clear the entire journal

Key design decisions:

  • Opt-in: Commands default to journalable = false; gradual migration
  • Manual undo: Command authors provide the undo closure (no automatic state snapshotting)
  • Worker thread: Journal includes a worker thread for async exec phases
  • Linear undo/redo: Branching (fork) supported in the data model but not yet exposed
  • Self-contained: Transaction.h is copied/adapted from MeshulaLab, no external dependency

Daemon Modes

The labcmd2-daemon binary supports three transport modes:

Mode Flag Protocol Listening
JSON-RPC (default) (none) JSON-RPC 2.0 over stdio stdin/stdout
MCP --mcp Model Context Protocol over stdio stdin/stdout
HTTP REST --http [port] HTTP/1.1, CORS-enabled TCP (default 8080)

Built-in Domains

Domain Commands Description
system ping, info, help, undo, redo, history, journal-clear Introspection, control, and journal management
test add, echo, concat, repeat Parameterized test commands (all journaled)

REST API

When running in --http mode, the daemon auto-generates routes from the registry:

Method Path Description
GET /api/v1/ Route manifest
GET /api/v1/health Health check (rate-limiting + worker pool stats)
GET /api/v1/metrics Request metrics (count, errors, latency)
GET /api/v1/openapi.json OpenAPI 3.0 spec
GET/POST /api/v1/<domain>/<subcommand> Auto-generated from registry

REST Hardening

The REST server includes the following hardening features:

Feature Details
ETag / Last-Modified GET responses include ETag (content hash) and Last-Modified (RFC 7231 date). Supports If-None-Match and If-Modified-Since304 Not Modified
Cache-Control GET: public, max-age=0, must-revalidate. POST/PUT/DELETE: no-store, no-cache
Rate Limiting Per-client IP, configurable window (default 100 req/60s). Returns 429 Too Many Requests with Retry-After header
Request Metrics /api/v1/metrics exposes total requests, error count, and per-endpoint average latency
Worker Pool Bounded thread pool with std::condition_variable semaphore (default 32 workers). No detached threads
Request ID Every response includes X-Request-Id (UUID) for tracing
CORS Full CORS support with Access-Control-Allow-Origin: * and OPTIONS preflight
Configurable backlog set_backlog() and set_idle_timeout() for connection tuning
Locale-independent parsing HTTP date parsing via LabText single-header library (no std::istringstream)

CLI Tool

The labcmd2-cli tool provides a metadata-driven interface to the daemon. All help text, schemas, and completions are generated at runtime from the command registry — no hardcoded strings.

Subcommands

Subcommand Description
`list [domains
help [<domain>[:<sub>]] Show help for domains, commands, or built-in CLI subcommands
schema <domain>:<sub> Emit JSON Schema for a command's parameters
call <domain>:<sub> [--json <json>] [--mcp] Execute a command via the daemon
start [--mcp] [--verbose] Fork daemon into background (FIFO-based stdio)
stop [--mcp] Stop running daemon
status [--mcp] Check if daemon is running
completion [--shell bash|zsh] Generate shell tab-completion script

Global Flags

Flag Description
--format <table|json|compact|tsv> Output format (also --json, --tsv, --compact shortcuts)
--verbose / -v Verbose mode (logs request/response JSON on stderr)
--quiet / -q Suppress non-essential output
--mcp Use MCP protocol mode

Tab Completion

Generate a completion script from your current registry metadata and install it:

Bash:

# Generate and install
labcmd2-cli completion --shell bash > ~/.labcmd2-completion.bash
echo 'source ~/.labcmd2-completion.bash' >> ~/.bashrc
source ~/.bashrc

Zsh:

# Generate and install (place in your fpath or source directly)
mkdir -p ~/.zsh/completion
labcmd2-cli completion --shell zsh > ~/.zsh/completion/_labcmd2
echo 'fpath=(~/.zsh/completion $fpath)' >> ~/.zshrc
echo 'autoload -Uz compinit && compinit' >> ~/.zshrc
source ~/.zshrc

The completion script provides:

  • First argument: top-level subcommands + domain names
  • Second argument after list/help: domain names
  • Second argument after schema/call: domain:subcommand pairs
  • --format flag: table, json, compact, tsv
  • --shell flag (for completion): bash, zsh

Daemon Communication

The CLI uses named pipes (FIFOs) for persistent daemon communication and auto-detects whether a daemon is running (via PID file) before falling back to one-shot subprocess mode. Stale PID files and crashed-daemon FIFOs are cleaned up automatically.

Library API

Using the CommandRegistry

#include "LabCmd2/Registry.h"#include "LabCmd2/StdioJSONRpcServer.h"
using namespace lab::cmd2;
// Register a domain with subcommands
auto &registry = CommandRegistry::instance();
DomainDescriptor domain;
domain.name = "myapp";
domain.description = "My application commands";
SubcommandDescriptor sub;
sub.name = "greet";
sub.full_command_name = "myapp:greet";
sub.handler = [](const std::vector<Argument> &args) -> Ack {
 Ack ack;
 std::string name = args.empty() ? "world" : args[0].stringArg;
 ack.payload.emplace_back("Hello, " + name + "!");
 return ack;
};
sub.description = "Greet someone";
sub.parameters = {
 ParameterDescriptor{"name", "string", false, "Name to greet", "world", {}, true, 0}
};
domain.subcommands.push_back(std::move(sub));
registry.register_domain(std::move(domain));
// Run as JSON-RPC server on stdio
StdioJSONRpcServer server(registry);
server.run();

Using the MCP Server

#include "LabCmd2/MCPStdioServer.h"
MCPStdioServer server(registry);
server.run(); // handles initialize → tools/list → tools/call lifecycle

Using the REST Server

#include "LabCmd2/RestServer.h"
RestServer server(registry, 8080);
server.start(); // thread-per-connection, keep-alive, CORS-enabled

Using the CLI Engine

#include "LabCmd2/CliEngine.h"
// Metadata-driven CLI: parses global flags, dispatches to list/help/schema/call
CliEngine engine(argc, argv);
auto result = engine.run();
if (result.success) {
 std::cout << result.output;
} else {
 std::cerr << result.error;
}

Project Structure

LabCmd2/
├── CMakeLists.txt # Build configuration (CMake + Ninja)
├── include/LabCmd2/
│ ├── LabCmd2.h # Platform export macros
│ ├── Command.h # Core types: Argument, Ack, Descriptor
│ ├── Transaction.h # Two-phase transaction model (exec/commit/undo)
│ ├── CommandJournal.h # Journal wrapper with worker thread
│ ├── Registry.h # CommandRegistry singleton (+ journal)
│ ├── StdioJSONRpcServer.h # JSON-RPC 2.0 stdio server
│ ├── MCPStdioServer.h # MCP protocol stdio server
│ ├── RestServer.h # HTTP/1.1 REST server (+ ETag, cache, rate limit)
│ ├── CliEngine.h # Metadata-driven CLI engine
│ ├── MockDomains.h # Idealized mock domains (testing)
│ └── BuiltInDomains.h # Built-in domain registration
├── include/LabText/
│ └── LabText.h # Bundled LabText (locale-independent parsing)
├── src/
│ ├── Command.cpp # Argument serialization, JSON Schema
│ ├── CommandJournal.cpp # Journal node management, undo/redo, worker
│ ├── Registry.cpp # Registry: register, lookup, execute (+ journal)
│ ├── StdioJSONRpcServer.cpp # JSON-RPC parsing and dispatch
│ ├── MCPStdioServer.cpp # MCP lifecycle and tool routing
│ ├── RestServer.cpp # HTTP server, ETag, cache, rate limit, metrics
│ ├── CliEngine.cpp # CLI flag parsing, subcommand dispatch
│ ├── MockDomains.cpp # Mock domain handlers (rope, triple, ...)
│ ├── BuiltInDomains.cpp # Built-in domain handlers
│ ├── LabTextODR.cpp # LabText ODR activation (bundled fallback)
│ ├── daemon_main.cpp # Daemon entry point (3 modes + journal cmds)
│ └── cli_main.cpp # CLI entry point (start/stop/call)
└── tests/
 ├── test_harness.h # Goldberg test framework
 ├── test_rest_server.cpp # REST server unit tests
 └── test_cli_engine.cpp # CLI engine and mock domain tests

Build

Prerequisites

  • C++17 compiler (Clang 14+ or GCC 11+)
  • CMake 3.21+
  • Ninja (recommended)

Build Commands

mkdir build && cd build
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DLABCMD2_BUILD_TESTS=ON
ninja

CMake Options

Flag Default Description
LABCMD2_BUILD_SHARED OFF Build shared library
LABCMD2_BUILD_DAEMON ON Build daemon and CLI executables
LABCMD2_BUILD_TESTS OFF Build unit tests

Dependencies

Dependency Version Delivery
nlohmann/json 3.11.3 CMake FetchContent (header-only)
LabText Bundled single-header (fallback if find_package(LabText) not found)

No other external runtime dependencies.

Testing

cd build
ctest --output-on-failure
Test Suite Coverage
test_rest_server Registry setup, command execution, route index bugs, REST server construction
test_cli_engine CLI flag parsing, domain listing, help formatting, JSON/TSV/compact output, schema generation, mock domain handlers

Design Principles

  • Single source of truthSubcommandDescriptor drives CLI help, MCP JSON Schema, HTTP routes, and JSON-RPC dispatch
  • Discoverability — every endpoint can enumerate domains, commands, parameters, and types from registry metadata
  • On-the-fly endpoint creation — define once, expose everywhere (CLI, REST, MCP, JSON-RPC)
  • Dynamic help — all help text, schemas, and documentation generated at runtime from metadata
  • Thread-safe registrystd::mutex protects registration; designed for single-threaded event loop dispatch
  • Opt-in journaling — commands opt into undo/redo via journalable flag; gradual migration, no forced reversibility

Current Status

Phase Status Tests
Phase 1: Core library + JSON-RPC daemon Complete
Phase 2: MCP stdio support Complete
Phase 3: CLI integration (FIFO-based persistent daemon) Complete
Phase 4a: HTTP REST server + hardening Complete 27 integration, 4 unit
Phase 4b: Transaction/Journal (undo/redo) Complete smoke-tested
Phase 4c: REST hardening (ETag, cache, rate limit, metrics) Complete E2E verified
Foundation: CLI engine + mock domains Complete 186 CLI + 4 REST tests

Transaction/Journal Integration

Completed by copying and adapting the Transaction/JournalProvider architecture from MeshulaLab:

  • Transaction.h — two-phase model (exec/commit/undo) with JournalNode linked-list chain
  • CommandJournal — journal wrapper with worker thread for async exec phases
  • SubcommandDescriptor extended with journalable, undo, and transaction_message fields
  • CommandRegistry::execute() wraps journaled commands in transactions
  • system:undo, system:redo, system:history, system:journal-clear registered in daemon
  • Debug logging on stderr at all lifecycle points (enqueue, commit, append, undo, redo, history, clear)

Foundation Phase

  • Refactor cli_main.cpp to use CliEngine
  • REST: ETag/Last-Modified, metrics endpoint, rate limiting, cache headers
  • REST: Worker pool (bounded, no detached threads), configurable backlog/idle timeout
  • REST: LabText integration for locale-independent HTTP date parsing
  • CLI hardening: list, help, schema, tab-completion, verbose mode, error messages (Priority 1)
  • MCP over HTTP (SSE endpoint alongside stdio) (Priority 3)
  • RPC/IPC (Unix domain sockets, batch requests, streaming) (Priority 4)

Deferred (post-foundation)

  • LabDb9 integration (real rope/entity/triadic domains)
  • Inception-MCP tool porting (20+ tools)
  • pybind11 Python bindings
  • WebSocket support

License

BSD 2-Clause ©2026 Nick Porcino