Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
985e6a4
fix(oauth,services): port #152 — DCR redirect URI tolerance + clippy
crimsonsunset May 19, 2026
7bf4cb1
docs(planning): add plan for dynamic MCP toggle meta tools
crimsonsunset May 19, 2026
5e04746
docs: add macOS build-from-source and app swap guide
crimsonsunset May 19, 2026
e72b64e
feat(gateway): add SessionOverrideRegistry and list-path composition
crimsonsunset May 19, 2026
f29543a
feat(meta-tools): add mcpmux_list_servers read tool (Phase 2)
crimsonsunset May 19, 2026
0905d38
feat(meta-tools): add session-scope enable/disable server tools (Phas…
crimsonsunset May 19, 2026
b29d5e8
feat(meta-tools): add workspace-scope enable/disable server tools (Ph…
crimsonsunset May 19, 2026
b590b0b
feat(meta-tools): session override UI and settings toggle (Phase 5)
crimsonsunset May 19, 2026
5730b25
docs(meta-tools): close Phase 5 with README and planning reconciliation
crimsonsunset May 19, 2026
726492e
feat(ui): add rename/edit for spaces, feature sets, and workspace bin…
crimsonsunset May 19, 2026
11814c4
feat(ui): link dashboard stat cards to detail pages
crimsonsunset May 19, 2026
da91fd9
docs: rename build-from-source-macos.md to run-from-source-macos.md
crimsonsunset May 19, 2026
2edcd29
docs(run-from-source): document pnpm dev watch flow
crimsonsunset May 19, 2026
5269a18
fix(gateway): apply session overrides in call_tool authorization
crimsonsunset May 22, 2026
d519a79
fix(gateway): forward structuredContent in proxied tool results
crimsonsunset May 23, 2026
7d762a8
docs(planning): add server account clones implementation plan
crimsonsunset May 23, 2026
55d0758
feat(server-clone): Phase 1 — Core clone API + storage
crimsonsunset May 23, 2026
94decc8
feat(server-clone): Phase 2 — Clone wizard UI
crimsonsunset May 23, 2026
a45d34c
feat(server-clone): Phase 3 — Meta-tool + docs surfacing
crimsonsunset May 23, 2026
c090948
feat(server-clone): Phase 4 — Validation + edge cases
crimsonsunset May 23, 2026
b7016de
chore(fmt): apply cargo fmt after validate sweep
crimsonsunset May 23, 2026
308963b
fix(desktop): restore custom titlebar window dragging
crimsonsunset May 23, 2026
36a4922
feat(desktop): add copy-all to server log viewer
crimsonsunset May 23, 2026
8ff352b
feat(desktop): expand/collapse all servers and disable on error
crimsonsunset May 23, 2026
b641acd
feat(desktop): My Servers search, filters popover, and shared UI prim…
crimsonsunset May 25, 2026
6215633
feat(desktop): add server count summary and smarter hover tooltips
crimsonsunset May 25, 2026
6b70c0f
feat(servers): add per-install display name override
crimsonsunset May 25, 2026
cc50654
feat(servers): smarter OAuth startup and sticky My Servers toolbar
crimsonsunset May 25, 2026
d3940bb
docs(planning): tool-level session pin meta tool
crimsonsunset May 25, 2026
9e4c3cd
docs(planning): web admin mode for remote UI access
crimsonsunset May 25, 2026
2052bcc
feat(servers): replace enable/disable buttons with labeled toggle
crimsonsunset May 25, 2026
f767e79
docs(planning): add meta-gateway invoke model for agent tool discovery
crimsonsunset May 25, 2026
4978d70
feat(gateway): Phase A — Meta invoke core
crimsonsunset May 25, 2026
4d11112
feat(gateway): Phase B — Result shaping on invoke
crimsonsunset May 25, 2026
270921d
feat(gateway): Phase C — FeatureSet invoke ACL + surfaced tools
crimsonsunset May 25, 2026
ed8a5fb
fix(gateway): upsert workspace binding in bind_current_workspace
crimsonsunset May 25, 2026
993f378
docs(planning): add meta-gateway invoke manual QA runbook
crimsonsunset May 25, 2026
c505b07
docs(planning): reconcile meta-gateway invoke QA progress
crimsonsunset May 25, 2026
433e7bd
fix(gateway): make invoke result truncation opt-in via filter
crimsonsunset May 25, 2026
01b60e3
docs(planning): record Phase 5 QA pass and PR #155
crimsonsunset May 25, 2026
3bc4dc9
test(gateway): cover invoke filter shaping with unit and e2e tests
crimsonsunset May 25, 2026
22d92c3
fix(gateway): allow direct call_tool for surfaced backend tools
crimsonsunset May 25, 2026
5508c5e
docs(planning): complete meta-gateway invoke manual QA sign-off
crimsonsunset May 25, 2026
6884742
docs(planning): sync meta-gateway invoke status with QA sign-off
crimsonsunset May 25, 2026
85113e7
fix(gateway): improve meta-tool DX for ACL, schema batch, and max_bytes
crimsonsunset May 25, 2026
b7626fb
docs(planning): sign off meta-gateway invoke targeted retest
crimsonsunset May 25, 2026
b9d2370
docs(planning): fork integration guide and session readiness plans
crimsonsunset May 25, 2026
51d8a1b
docs: remove personal fork planning docs from upstream tree
crimsonsunset May 27, 2026
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
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Lightweight and cross-platform — built in Rust with Tauri 2, McpMux uses minim
}
```

**3.** Done. Every tool from every server is available in every client, right now.
**3.** Done. Connected clients see a small fixed meta-tool surface (~12 `mcpmux_*` tools). Backend tools are discovered via **`mcpmux_search_tools`** → **`mcpmux_get_tool_schema`** → **`mcpmux_invoke_tool`**, keeping context windows lean. Optionally surface individual hot-path tools into `tools/list` per FeatureSet.

McpMux routes calls to the right server, refreshes OAuth tokens automatically, and keeps credentials encrypted in your OS keychain — you never think about it again.

Expand Down Expand Up @@ -109,10 +109,37 @@ Create isolated Spaces — each with their own servers, credentials, and permiss

### Control What Each Client Can Do

Not every AI client should have the same power. Create Feature Sets — permission bundles that control exactly which tools, prompts, and resources a client can access. Build a "Read Only" set for cautious workflows, a "React Development" set with just GitHub and Filesystem, or a "Full Stack Dev" set with everything. Assign them per-client so each tool only goes where you want it.
Not every AI client should have the same power. Create Feature Sets — permission bundles that control exactly which tools a client can **invoke** (search + invoke ACL). In the editor, the **checkbox** includes a tool in that ACL; the **Surface** button (optional, per row) promotes an included tool into the client's `tools/list` for one-hop hot paths. Build a "Read Only" set for cautious workflows, a "React Development" set with just GitHub and Filesystem, or a "Full Stack Dev" set with everything. Assign them per-client so each tool only goes where you want it.

![Feature Sets — granular per-server tool selection](docs/screenshots/featureset-detail.png)

### Self-Management Meta Tools (mcpmux_*)

Connected AI clients see a fixed ~12-tool meta surface instead of every backend tool definition. FeatureSets define what is **invokable**; optional **surfaced** tools (0–N per set) can be promoted into `tools/list` for one-hop hot paths. Workspace bindings pin stable per-folder toolsets; session enable/disable gates server activity without bloating context.

McpMux exposes a built-in `mcpmux_*` tool namespace for search → schema → invoke workflows:

1. Call **`mcpmux_list_servers`** — server-level manifest with per-server status: `enabled_via_binding`, `enabled_via_session`, `disabled_via_session`, or `inactive`.
2. Call **`mcpmux_enable_server`** or **`mcpmux_disable_server`** — toggle servers on or off for the session or workspace.
3. Call **`mcpmux_search_tools`** — find invokable tools by query (respects FeatureSet ACL).
4. Call **`mcpmux_get_tool_schema`** — load parameter schemas before invoking.
5. Call **`mcpmux_invoke_tool`** — invoke any permitted backend tool through one entry point.

| Tool | Type | Purpose |
| ---- | ---- | ------- |
| `mcpmux_list_all_tools` | read | Full tool roster in the resolved Space (diagnostic) |
| `mcpmux_list_feature_sets` | read | FeatureSets available in the resolved Space |
| `mcpmux_list_servers` | read | Server-level manifest with status |
| `mcpmux_search_tools` | read | Search invokable tools with optional schema detail |
| `mcpmux_get_tool_schema` | read | Load input schemas before invoke |
| `mcpmux_invoke_tool` | read | Invoke a backend tool by server_id + tool name |
| `mcpmux_enable_server` | write | Enable a server (session or workspace scope) |
| `mcpmux_disable_server` | write | Disable a server (session or workspace scope) |
| `mcpmux_create_feature_set` | write | Create a custom FeatureSet (optional `surfaced_tools[]`) |
| `mcpmux_bind_current_workspace` | write | Bind the session's workspace root to FeatureSets |

In the desktop app: **Settings → Self-management tools** toggles the whole namespace and optional approval for session-scope overrides. **Workspaces → live folder inspector → Active session overrides** shows per-session enabled/disabled servers and lets you clear overrides with one click.

### See and Manage Every Connected Client

Cursor, VS Code, Windsurf, Claude Code — see every AI client connected to your gateway in real time. Click any client to manage its workspace, grant or revoke feature sets, and see exactly which tools it can access. New clients authenticate via OAuth with a one-click approval flow.
Expand Down
9 changes: 5 additions & 4 deletions apps/desktop/src-tauri/src/commands/feature_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct FeatureSetMemberResponse {
pub member_type: String,
pub member_id: String,
pub mode: String,
pub surfaced: bool,
}

impl From<&FeatureSetMember> for FeatureSetMemberResponse {
Expand All @@ -32,6 +33,7 @@ impl From<&FeatureSetMember> for FeatureSetMemberResponse {
member_type: m.member_type.as_str().to_string(),
member_id: m.member_id.clone(),
mode: m.mode.as_str().to_string(),
surfaced: m.surfaced,
}
}
}
Expand Down Expand Up @@ -92,6 +94,7 @@ pub struct AddMemberInput {
pub member_type: String, // "feature" or "feature_set"
pub member_id: String,
pub mode: Option<String>, // "include" or "exclude", defaults to "include"
pub surfaced: Option<bool>,
}

/// List all feature sets.
Expand Down Expand Up @@ -263,10 +266,6 @@ pub async fn update_feature_set(
.map_err(|e| e.to_string())?
.ok_or("Feature set not found")?;

if feature_set.is_builtin {
return Err("Cannot modify builtin feature set".to_string());
}

if let Some(name) = input.name {
feature_set.name = name;
}
Expand Down Expand Up @@ -372,6 +371,7 @@ pub async fn add_feature_set_member(
member_type,
member_id: input.member_id,
mode,
surfaced: input.surfaced.unwrap_or(false),
};

feature_set.members.push(member);
Expand Down Expand Up @@ -496,6 +496,7 @@ pub async fn set_feature_set_members(
member_type,
member_id: input.member_id,
mode,
surfaced: input.surfaced.unwrap_or(false),
}
})
.collect();
Expand Down
26 changes: 24 additions & 2 deletions apps/desktop/src-tauri/src/commands/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ pub struct GatewayAppState {
/// Surfaced to the desktop Workspaces tab so users can see + act on
/// every folder connected clients are currently operating in.
pub session_roots: Option<Arc<mcpmux_gateway::services::SessionRootsRegistry>>,
/// Session-scoped server enable/disable overrides (meta-tool mutations).
pub session_overrides: Option<Arc<mcpmux_gateway::services::SessionOverrideRegistry>>,
/// Per-session list_changed bridge — used when the UI clears overrides.
pub mcp_notifier: Option<Arc<mcpmux_gateway::consumers::MCPNotifier>>,
}

/// Gracefully shuts down a running gateway and waits for the axum task
Expand Down Expand Up @@ -888,6 +892,8 @@ pub async fn start_gateway(
let server_manager = server.server_manager();
let grant_service = server.grant_service();
let session_roots = server.session_roots();
let session_overrides = server.session_overrides();
let mcp_notifier = server.notification_bridge();

// Subscribe to OAuth completions BEFORE spawn so we don't miss early
// events emitted during initial auto-connect.
Expand Down Expand Up @@ -935,6 +941,8 @@ pub async fn start_gateway(
state.grant_service = Some(grant_service);
state.approval_broker = Some(approval_broker);
state.session_roots = Some(session_roots);
state.session_overrides = Some(session_overrides);
state.mcp_notifier = Some(mcp_notifier);
info!(
"[Gateway] Started — url={}, event_emitter={}, grant_service={}",
url,
Expand Down Expand Up @@ -1567,11 +1575,12 @@ pub async fn connect_all_enabled_servers(
errors: vec![],
};

for (server_info, transport, _server_definition, _installed) in servers_to_connect {
for (server_info, transport, _server_definition, installed) in servers_to_connect {
let space_uuid = server_info.space_id;
let server_id = server_info.server_id.clone();

let ctx = ConnectionContext::new(space_uuid, server_id.clone(), transport);
let ctx = ConnectionContext::new(space_uuid, server_id.clone(), transport)
.with_auto_reconnect(true);
match pool_service.connect_server(&ctx).await {
ConnectionResult::Connected { reused, features } => {
if reused {
Expand All @@ -1580,6 +1589,19 @@ pub async fn connect_all_enabled_servers(
result.connected += 1;
}

if server_info.requires_oauth && !installed.oauth_connected {
if let Err(e) = app_state
.installed_server_repository
.set_oauth_connected(&installed.id, true)
.await
{
warn!(
"[Gateway] Connected {} but failed to set oauth_connected: {}",
server_id, e
);
}
}

info!(
"[Gateway] Connected {} (reused: {}, features: {})",
server_id,
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ pub mod logs;
pub mod meta_tool_approval;
pub mod oauth;
pub mod server;
pub mod server_clone;
pub mod server_discovery;
pub mod server_feature;
pub mod server_manager;
pub mod session_overrides;
pub mod settings;
pub mod space;
pub mod workspace_binding;
Expand All @@ -32,9 +34,11 @@ pub use logs::*;
pub use meta_tool_approval::*;
pub use oauth::*;
pub use server::*;
pub use server_clone::*;
pub use server_discovery::*;
pub use server_feature::*;
pub use server_manager::*;
pub use session_overrides::*;
pub use settings::*;
pub use space::*;
pub use workspace_binding::*;
27 changes: 27 additions & 0 deletions apps/desktop/src-tauri/src/commands/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub async fn set_server_oauth_connected(
.map_err(|e| e.to_string())
}

#[allow(clippy::too_many_arguments)]
#[tauri::command]
pub async fn save_server_inputs(
app_service: State<'_, Arc<RwLock<Option<ServerAppService>>>>,
Expand All @@ -136,6 +137,7 @@ pub async fn save_server_inputs(
env_overrides: Option<HashMap<String, String>>,
args_append: Option<Vec<String>>,
extra_headers: Option<HashMap<String, String>>,
display_name_override: Option<String>,
) -> Result<InstalledServer, String> {
let service_lock = app_service.read().await;
let service = service_lock
Expand All @@ -152,7 +154,32 @@ pub async fn save_server_inputs(
env_overrides,
args_append,
extra_headers,
display_name_override,
)
.await
.map_err(|e| e.to_string())
}

/// Set or clear the user-supplied display label on an installed server.
///
/// Empty/whitespace clears the override and the UI falls back to the cached
/// definition name. Does not change `server_id`, alias, or tool prefixes.
#[tauri::command]
pub async fn set_server_display_name(
app_service: State<'_, Arc<RwLock<Option<ServerAppService>>>>,
id: String,
space_id: String,
display_name: Option<String>,
) -> Result<InstalledServer, String> {
let service_lock = app_service.read().await;
let service = service_lock
.as_ref()
.ok_or("ServerAppService not initialized")?;

let space_uuid = uuid::Uuid::parse_str(&space_id).map_err(|e| e.to_string())?;

service
.set_display_name_override(space_uuid, &id, display_name)
.await
.map_err(|e| e.to_string())
}
99 changes: 99 additions & 0 deletions apps/desktop/src-tauri/src/commands/server_clone.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Server clone commands

use mcpmux_core::application::ServerAppService;
use mcpmux_core::domain::InstalledServer;
use std::sync::Arc;
use tauri::State;
use tokio::sync::RwLock;

/// Clone an installed server into a new suffixed manual-entry install in the same space.
///
/// `display_name` (optional) is stored as `display_name_override` so the user-supplied
/// label survives later definition refreshes (e.g. user-config sync). When omitted, the
/// auto `"Source (suffix)"` label on the cached definition is used as fallback.
#[tauri::command]
pub async fn clone_server(
app_service: State<'_, Arc<RwLock<Option<ServerAppService>>>>,
space_id: String,
source_server_id: String,
suffix: String,
alias: Option<String>,
display_name: Option<String>,
) -> Result<InstalledServer, String> {
let service_lock = app_service.read().await;
let service = service_lock
.as_ref()
.ok_or("ServerAppService not initialized")?;

let space_uuid = uuid::Uuid::parse_str(&space_id).map_err(|e| e.to_string())?;

service
.clone_server(
space_uuid,
&source_server_id,
&suffix,
alias.as_deref(),
display_name.as_deref(),
)
.await
.map_err(|e| e.to_string())
}

/// Return whether a suffixed clone ID is available in the given space.
#[tauri::command]
pub async fn is_clone_id_available(
app_service: State<'_, Arc<RwLock<Option<ServerAppService>>>>,
space_id: String,
source_server_id: String,
suffix: String,
) -> Result<bool, String> {
let service_lock = app_service.read().await;
let service = service_lock
.as_ref()
.ok_or("ServerAppService not initialized")?;

let space_uuid = uuid::Uuid::parse_str(&space_id).map_err(|e| e.to_string())?;

service
.is_clone_id_available(space_uuid, &source_server_id, &suffix)
.await
.map_err(|e| e.to_string())
}

/// Suggest the first available default suffix for cloning a server.
#[tauri::command]
pub async fn suggest_clone_suffix(
app_service: State<'_, Arc<RwLock<Option<ServerAppService>>>>,
space_id: String,
source_server_id: String,
) -> Result<String, String> {
let service_lock = app_service.read().await;
let service = service_lock
.as_ref()
.ok_or("ServerAppService not initialized")?;

let space_uuid = uuid::Uuid::parse_str(&space_id).map_err(|e| e.to_string())?;

service
.suggest_clone_suffix(space_uuid, &source_server_id)
.await
.map_err(|e| e.to_string())
}

/// List installed servers in a space that were cloned from the given source.
#[tauri::command]
pub async fn list_clone_dependents(
app_service: State<'_, Arc<RwLock<Option<ServerAppService>>>>,
space_id: String,
source_server_id: String,
) -> Result<Vec<InstalledServer>, String> {
let service_lock = app_service.read().await;
let service = service_lock
.as_ref()
.ok_or("ServerAppService not initialized")?;

service
.list_clone_dependents(&space_id, &source_server_id)
.await
.map_err(|e| e.to_string())
}
Loading