Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,15 @@ let h2 = spawn_mux_server(MuxConfig::new("/tmp/b.sock", "server-b")).await?;
## [0.2.0] - 2025-11-24

### Added
- Optional tray icon (`--tray`) showing live server status, client and pending counts, and restart reasons. ([5eefde4](https://github.com/Loctree/rmcp-mux/commit/5eefde4))
- Config file support (JSON/YAML/TOML) with auto-detection and CLI overrides. ([5eefde4](https://github.com/Loctree/rmcp-mux/commit/5eefde4))
- `rmcp_mux_proxy` helper binary plus launchd template and installer tweaks for easier setup. ([04e5402](https://github.com/Loctree/rmcp-mux/commit/04e5402))
- GitHub Actions CI workflow for formatting, linting, testing, and coverage, including an async proxy forwarding test. ([ad2b9aa](https://github.com/Loctree/rmcp-mux/commit/ad2b9aa))
- Mux hooks, Semgrep rules, and expanded README documentation. ([e80083c](https://github.com/Loctree/rmcp-mux/commit/e80083c))
- Optional tray icon (`--tray`) showing live server status, client and pending counts, and restart reasons. ([5eefde4](https://github.com/vetcoders/rmcp-mux/commit/5eefde4))
- Config file support (JSON/YAML/TOML) with auto-detection and CLI overrides. ([5eefde4](https://github.com/vetcoders/rmcp-mux/commit/5eefde4))
- `rmcp_mux_proxy` helper binary plus launchd template and installer tweaks for easier setup. ([04e5402](https://github.com/vetcoders/rmcp-mux/commit/04e5402))
- GitHub Actions CI workflow for formatting, linting, testing, and coverage, including an async proxy forwarding test. ([ad2b9aa](https://github.com/vetcoders/rmcp-mux/commit/ad2b9aa))
- Mux hooks, Semgrep rules, and expanded README documentation. ([e80083c](https://github.com/vetcoders/rmcp-mux/commit/e80083c))
- `health` subcommand to resolve config and assert socket reachability, plus unit tests for healthy/missing sockets.

### Changed
- Refactored mux state management and tray functionality into dedicated `state` and `tray` modules, with tray dependencies gated behind an optional `tray` feature; CI updated to run with `--no-default-features`. ([0d60764](https://github.com/Loctree/rmcp-mux/commit/0d60764), [ad2b9aa](https://github.com/Loctree/rmcp-mux/commit/ad2b9aa))
- Refactored mux state management and tray functionality into dedicated `state` and `tray` modules, with tray dependencies gated behind an optional `tray` feature; CI updated to run with `--no-default-features`. ([0d60764](https://github.com/vetcoders/rmcp-mux/commit/0d60764), [ad2b9aa](https://github.com/vetcoders/rmcp-mux/commit/ad2b9aa))

## [0.1.5] - 2025-11-20

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.4.0"
edition = "2024"
description = "MCP server multiplexer - single server, multiple clients via Unix sockets"
license = "MIT"
repository = "https://github.com/Loctree/rmcp-mux"
repository = "https://github.com/vetcoders/rmcp-mux"
keywords = ["mcp", "multiplexer", "rpc", "unix-socket"]
categories = ["network-programming", "development-tools"]
readme = "README.md"
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# rmcp-mux – MCP Server Multiplexer

[![CI](https://github.com/Loctree/rmcp-mux/actions/workflows/ci.yml/badge.svg)](https://github.com/Loctree/rmcp-mux/actions/workflows/ci.yml)
[![CI](https://github.com/vetcoders/rmcp-mux/actions/workflows/ci.yml/badge.svg)](https://github.com/vetcoders/rmcp-mux/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/rmcp-mux.svg)](https://crates.io/crates/rmcp-mux)
[![Version](https://img.shields.io/badge/version-0.3.4-blue.svg)](Cargo.toml)
Comment on lines +3 to 5
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
Expand Down Expand Up @@ -79,7 +79,7 @@ cargo build --release

### One-liner (curl | sh)
```bash
curl -fsSL https://raw.githubusercontent.com/Loctree/rmcp-mux/main/tools/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/vetcoders/rmcp-mux/main/tools/install.sh | sh
```

**Environment overrides:**
Expand Down Expand Up @@ -119,11 +119,11 @@ cmd = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/docs"]
lazy_start = true

[servers.rmcp-memex]
socket = "~/.rmcp-servers/sockets/rmcp-memex.sock"
cmd = "/path/to/rmcp-memex"
args = ["serve", "--config", "config.toml", "--db-path", "~/.ai-memories/lancedb"]
env = { SLED_PATH = "~/.rmcp-servers/sled/memex" }
[servers.custom-db]
socket = "~/mcp-sockets/custom-db.sock"
cmd = "/path/to/custom-mcp-server"
args = ["serve", "--config", "config.toml", "--db-path", "~/.local/share/custom-db"]
env = { DB_PATH = "~/.local/share/custom-db/store" }
Comment on lines +125 to +126

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the example configuration, ~ is used in the args and env fields (e.g., ~/.local/share/custom-db). However, rmcp-mux only performs tilde (~) expansion on socket and status_file paths (via expand_path in src/config.rs). Since the child process is spawned directly without a shell, any ~ in args or env will be passed literally to the child process and will not be expanded, which can cause the spawned server to fail. It is safer to use absolute paths or placeholders like /path/to/... in the example to avoid misleading users.

Suggested change
args = ["serve", "--config", "config.toml", "--db-path", "~/.local/share/custom-db"]
env = { DB_PATH = "~/.local/share/custom-db/store" }
args = ["serve", "--config", "config.toml", "--db-path", "/path/to/custom-db"]
env = { DB_PATH = "/path/to/custom-db/store" }

lazy_start = false
```

Expand Down Expand Up @@ -152,9 +152,9 @@ MCP hosts expecting STDIO communication connect through `rmcp-mux-proxy`:
```json
{
"mcpServers": {
"rmcp-memex": {
"memory": {
"command": "rmcp-mux-proxy",
"args": ["--socket", "~/.rmcp-servers/sockets/rmcp-memex.sock"]
"args": ["--socket", "~/.rmcp-servers/sockets/memory.sock"]
},
Comment on lines +155 to 158
"loctree": {
"command": "rmcp-mux-proxy",
Expand Down
3 changes: 1 addition & 2 deletions docs/dev/guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ error messages or tests.

### 1.3. Building the binaries

All builds are driven through Cargo. From the project root (`/Users/maciejgad/.rmcp_servers/rmcp-mux` in the current
setup):
All builds are driven through Cargo. From the project root (`~/rmcp-mux`, for example):

#### 1.3.1. Debug build (fast, for development)

Expand Down
3 changes: 1 addition & 2 deletions src/runtime/heartbeat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
//! It periodically sends lightweight ping probes and tracks response times,
//! triggering server restarts when the backend becomes unresponsive.
//!
//! Created by M&K (c)2025 The LibraxisAI Team
//! Co-Authored-By: [Maciej](void@div0.space) & [Klaudiusz](the1st@whoai.am)
//! Created by Vetcoders (c)2025

use std::sync::Arc;
use std::time::{Duration, Instant};
Expand Down
3 changes: 3 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,9 @@ pub async fn reset_state(
fn compute_health_status(st: &MuxState, active_clients: usize) -> HealthStatus {
match &st.server_status {
ServerStatus::Running => {
// Explicit guard-then-divide reads clearer than checked_div here:
// we want a plain 0 default, not an Option. Behaviour unchanged.
#[allow(clippy::manual_checked_ops)]
let load_pct = if st.max_active_clients > 0 {
(active_clients * 100) / st.max_active_clients
} else {
Expand Down
172 changes: 77 additions & 95 deletions src/wizard/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,94 +45,82 @@ pub fn handle_key(app: &mut AppState, key: KeyEvent) -> Result<bool> {
}

// Tab to switch panels
KeyCode::Tab => {
if app.editing.is_none() {
app.active_panel = match app.active_panel {
Panel::ServiceList => Panel::Editor,
Panel::Editor => Panel::ServiceList,
Panel::ConfirmDialog => Panel::ConfirmDialog,
};
update_message(app);
}
KeyCode::Tab if app.editing.is_none() => {
app.active_panel = match app.active_panel {
Panel::ServiceList => Panel::Editor,
Panel::Editor => Panel::ServiceList,
Panel::ConfirmDialog => Panel::ConfirmDialog,
};
update_message(app);
}

KeyCode::BackTab => {
if app.editing.is_none() {
app.active_panel = match app.active_panel {
Panel::ServiceList => Panel::Editor,
Panel::Editor => Panel::ServiceList,
Panel::ConfirmDialog => Panel::ConfirmDialog,
};
update_message(app);
}
KeyCode::BackTab if app.editing.is_none() => {
app.active_panel = match app.active_panel {
Panel::ServiceList => Panel::Editor,
Panel::Editor => Panel::ServiceList,
Panel::ConfirmDialog => Panel::ConfirmDialog,
};
update_message(app);
}

// Navigation
KeyCode::Up => {
if app.editing.is_none() {
match (app.wizard_step, app.active_panel) {
(WizardStep::ServerSelection, Panel::ServiceList) => {
if app.selected_service > 0 {
sync_form_to_service(app);
app.selected_service -= 1;
load_service_to_form(app);
}
}
(WizardStep::ServerSelection, Panel::Editor) => {
app.current_field = previous_field(app.current_field);
}
(WizardStep::ClientSelection, Panel::ServiceList) => {
if app.selected_client > 0 {
app.selected_client -= 1;
}
}
(WizardStep::Confirmation, _) => {
// Navigate through save options
app.confirm_choice = previous_confirm_choice(app.confirm_choice);
}
(WizardStep::HealthCheck, _) => {
// Toggle between Ok and TryAgain
app.health_choice = match app.health_choice {
HealthCheckChoice::Ok => HealthCheckChoice::TryAgain,
HealthCheckChoice::TryAgain => HealthCheckChoice::Ok,
};
}
_ => {}
KeyCode::Up if app.editing.is_none() => {
match (app.wizard_step, app.active_panel) {
(WizardStep::ServerSelection, Panel::ServiceList) if app.selected_service > 0 => {
sync_form_to_service(app);
app.selected_service -= 1;
load_service_to_form(app);
}
(WizardStep::ServerSelection, Panel::Editor) => {
app.current_field = previous_field(app.current_field);
}
(WizardStep::ClientSelection, Panel::ServiceList) if app.selected_client > 0 => {
app.selected_client -= 1;
}
(WizardStep::Confirmation, _) => {
// Navigate through save options
app.confirm_choice = previous_confirm_choice(app.confirm_choice);
}
(WizardStep::HealthCheck, _) => {
// Toggle between Ok and TryAgain
app.health_choice = match app.health_choice {
HealthCheckChoice::Ok => HealthCheckChoice::TryAgain,
HealthCheckChoice::TryAgain => HealthCheckChoice::Ok,
};
}
_ => {}
}
}

KeyCode::Down => {
if app.editing.is_none() {
match (app.wizard_step, app.active_panel) {
(WizardStep::ServerSelection, Panel::ServiceList) => {
if app.selected_service < app.services.len().saturating_sub(1) {
sync_form_to_service(app);
app.selected_service += 1;
load_service_to_form(app);
}
}
(WizardStep::ServerSelection, Panel::Editor) => {
app.current_field = next_field(app.current_field);
}
(WizardStep::ClientSelection, Panel::ServiceList) => {
if app.selected_client < app.clients.len().saturating_sub(1) {
app.selected_client += 1;
}
}
(WizardStep::Confirmation, _) => {
// Navigate through save options
app.confirm_choice = next_confirm_choice(app.confirm_choice);
}
(WizardStep::HealthCheck, _) => {
// Toggle between Ok and TryAgain
app.health_choice = match app.health_choice {
HealthCheckChoice::Ok => HealthCheckChoice::TryAgain,
HealthCheckChoice::TryAgain => HealthCheckChoice::Ok,
};
}
_ => {}
KeyCode::Down if app.editing.is_none() => {
match (app.wizard_step, app.active_panel) {
(WizardStep::ServerSelection, Panel::ServiceList)
if app.selected_service < app.services.len().saturating_sub(1) =>
{
sync_form_to_service(app);
app.selected_service += 1;
load_service_to_form(app);
}
(WizardStep::ServerSelection, Panel::Editor) => {
app.current_field = next_field(app.current_field);
}
(WizardStep::ClientSelection, Panel::ServiceList)
if app.selected_client < app.clients.len().saturating_sub(1) =>
{
app.selected_client += 1;
}
(WizardStep::Confirmation, _) => {
// Navigate through save options
app.confirm_choice = next_confirm_choice(app.confirm_choice);
}
(WizardStep::HealthCheck, _) => {
// Toggle between Ok and TryAgain
app.health_choice = match app.health_choice {
HealthCheckChoice::Ok => HealthCheckChoice::TryAgain,
HealthCheckChoice::TryAgain => HealthCheckChoice::Ok,
};
}
_ => {}
}
}

Expand All @@ -153,14 +141,13 @@ pub fn handle_key(app: &mut AppState, key: KeyEvent) -> Result<bool> {
app.message = "Editing... Esc to finish".into();
}
}
(WizardStep::ClientSelection, Panel::ServiceList) => {
(WizardStep::ClientSelection, Panel::ServiceList)
// Toggle client selection on Enter as well
if app.selected_client < app.clients.len() {
if app.selected_client < app.clients.len() => {
app.clients[app.selected_client].selected =
!app.clients[app.selected_client].selected;
update_step2_message(app);
}
}
(WizardStep::Confirmation, _) => {
// Execute the selected action
return execute_confirm_choice(app);
Expand All @@ -176,9 +163,9 @@ pub fn handle_key(app: &mut AppState, key: KeyEvent) -> Result<bool> {
// Space toggles selection (in ServiceList for both steps) or tray (in Editor)
KeyCode::Char(' ') => {
match (app.wizard_step, app.active_panel) {
(WizardStep::ServerSelection, Panel::ServiceList) => {
(WizardStep::ServerSelection, Panel::ServiceList)
// Toggle selection for current server
if app.selected_service < app.services.len() {
if app.selected_service < app.services.len() => {
app.services[app.selected_service].selected =
!app.services[app.selected_service].selected;
let selected_count = app.services.iter().filter(|s| s.selected).count();
Expand All @@ -187,31 +174,26 @@ pub fn handle_key(app: &mut AppState, key: KeyEvent) -> Result<bool> {
selected_count
);
}
}
(WizardStep::ServerSelection, Panel::Editor) => {
if app.current_field == Field::Tray {
(WizardStep::ServerSelection, Panel::Editor)
if app.current_field == Field::Tray => {
app.form.tray = !app.form.tray;
app.form.dirty = true;
}
}
(WizardStep::ClientSelection, Panel::ServiceList) => {
(WizardStep::ClientSelection, Panel::ServiceList)
// Toggle selection for current client
if app.selected_client < app.clients.len() {
if app.selected_client < app.clients.len() => {
app.clients[app.selected_client].selected =
!app.clients[app.selected_client].selected;
update_step2_message(app);
}
}
_ => {}
}
}

// Escape
KeyCode::Esc => {
if app.editing.is_some() {
app.editing = None;
update_message(app);
}
KeyCode::Esc if app.editing.is_some() => {
app.editing = None;
update_message(app);
}

// Next step with 'n' key
Expand Down
4 changes: 2 additions & 2 deletions tools/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ umask 022

# rmcp-mux install script
# Usage:
# curl -fsSL https://raw.githubusercontent.com/Loctree/rmcp-mux/main/tools/install.sh | sh
# curl -fsSL https://raw.githubusercontent.com/vetcoders/rmcp-mux/main/tools/install.sh | sh
# Env overrides:
# INSTALL_DIR where to place the runnable `rmcp-mux` wrapper (default: $HOME/.local/bin)
# CARGO_HOME override cargo home (default: ~/.cargo)
Expand All @@ -14,7 +14,7 @@ umask 022
INSTALL_DIR=${INSTALL_DIR:-"$HOME/.local/bin"}
CARGO_HOME=${CARGO_HOME:-"$HOME/.cargo"}
CARGO_BIN="$CARGO_HOME/bin"
REPO_URL="https://github.com/LibraxisAI/rmcp-mux"
REPO_URL="https://github.com/vetcoders/rmcp-mux"
# Allow pinning a branch/tag/commit; defaults to main.
MUX_REF=${MUX_REF:-"main"}

Expand Down
Loading