| bindings | Add python and zig bindings, embedded endpoints, examples | |
| examples/rpn | Add python and zig bindings, embedded endpoints, examples | |
| include | Add python and zig bindings, embedded endpoints, examples | |
| src | Add python and zig bindings, embedded endpoints, examples | |
| tests | Add python and zig bindings, embedded endpoints, examples | |
| .gitignore | Add python and zig bindings, embedded endpoints, examples | |
| 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). | |
| README.md | Add command line tab completion and better REST error reporting | |
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
execphases - Linear undo/redo: Branching (fork) supported in the data model but not yet exposed
- Self-contained:
Transaction.his 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-Since → 304 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:subcommandpairs --formatflag:table,json,compact,tsv--shellflag (forcompletion):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 ®istry = 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 truth —
SubcommandDescriptordrives 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 registry —
std::mutexprotects registration; designed for single-threaded event loop dispatch - Opt-in journaling — commands opt into undo/redo via
journalableflag; 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) withJournalNodelinked-list chainCommandJournal— journal wrapper with worker thread for async exec phasesSubcommandDescriptorextended withjournalable,undo, andtransaction_messagefieldsCommandRegistry::execute()wraps journaled commands in transactionssystem:undo,system:redo,system:history,system:journal-clearregistered in daemon- Debug logging on stderr at all lifecycle points (enqueue, commit, append, undo, redo, history, clear)
Foundation Phase
- Refactor
cli_main.cppto useCliEngine - 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