Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions apps/desktop/src-tauri/src/commands/client_install.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! One-click IDE install commands.
//!
//! Opens deep link URIs for VS Code and Cursor to install the McpMux MCP server.

use mcpmux_core::{cursor_deep_link, vscode_deep_link};
use tracing::info;

/// Add McpMux to VS Code via deep link.
#[tauri::command]
pub async fn add_to_vscode(gateway_url: String) -> Result<(), String> {
let uri = vscode_deep_link(&gateway_url);
info!("[ClientInstall] Opening VS Code deep link: {}", uri);
open_deep_link(&uri)
}

/// Add McpMux to Cursor via deep link.
#[tauri::command]
pub async fn add_to_cursor(gateway_url: String) -> Result<(), String> {
let uri = cursor_deep_link(&gateway_url);
info!("[ClientInstall] Opening Cursor deep link: {}", uri);
open_deep_link(&uri)
}

/// Open a deep link URI using the system handler.
fn open_deep_link(uri: &str) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
open_url_shell_execute(uri)
}
#[cfg(not(target_os = "windows"))]
{
open::that(uri).map_err(|e| format!("Failed to open URI: {}", e))
}
}

/// Windows: Use ShellExecuteW to open URI without flashing a console window.
#[cfg(target_os = "windows")]
fn open_url_shell_execute(url: &str) -> Result<(), String> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::ptr;

#[link(name = "shell32")]
extern "system" {
fn ShellExecuteW(
hwnd: *mut std::ffi::c_void,
operation: *const u16,
file: *const u16,
parameters: *const u16,
directory: *const u16,
show_cmd: i32,
) -> isize;
}

let url_wide: Vec<u16> = OsStr::new(url).encode_wide().chain(Some(0)).collect();
let open_wide: Vec<u16> = OsStr::new("open").encode_wide().chain(Some(0)).collect();

let result = unsafe {
ShellExecuteW(
ptr::null_mut(),
open_wide.as_ptr(),
url_wide.as_ptr(),
ptr::null(),
ptr::null(),
1, // SW_SHOWNORMAL
)
};

if result > 32 {
Ok(())
} else {
Err(format!("ShellExecuteW failed with code: {}", result))
}
}
2 changes: 2 additions & 0 deletions apps/desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

pub mod client;
pub mod client_custom_features;
pub mod client_install;
pub mod config_export;
pub mod credential;
pub mod feature_members;
Expand All @@ -22,6 +23,7 @@ pub mod space;
// Re-export commands for convenience
pub use client::*;
pub use client_custom_features::*;
pub use client_install::*;
pub use config_export::*;
pub use feature_members::*;
pub use feature_set::*;
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,9 @@ pub fn run() {
commands::get_config_paths,
commands::check_config_exists,
commands::backup_existing_config,
// Client install commands (one-click IDE setup)
commands::add_to_vscode,
commands::add_to_cursor,
// Gateway commands
commands::get_gateway_status,
commands::start_gateway,
Expand Down
63 changes: 6 additions & 57 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
Settings,
Sun,
Moon,
Check,
Loader2,
FolderOpen,
FileText,
Expand All @@ -32,6 +31,7 @@ import { ThemeProvider } from '@/components/ThemeProvider';
import { OAuthConsentModal } from '@/components/OAuthConsentModal';
import { ServerInstallModal } from '@/components/ServerInstallModal';
import { SpaceSwitcher } from '@/components/SpaceSwitcher';
import { ConnectIDEs } from '@/components/ConnectIDEs';
import { useDataSync } from '@/hooks/useDataSync';
import { useAppStore, useActiveSpace, useViewSpace, useTheme } from '@/stores';
import { RegistryPage } from '@/features/registry';
Expand Down Expand Up @@ -337,7 +337,6 @@ function DashboardView() {
running: boolean;
url: string | null;
}>({ running: false, url: null });
const [exportSuccess, setExportSuccess] = useState<string | null>(null);
const viewSpace = useViewSpace();

// Load stats on mount and when gateway changes
Expand Down Expand Up @@ -505,61 +504,11 @@ function DashboardView() {
</Card>
</div>

{/* Client Config */}
<Card>
<CardHeader>
<CardTitle>Connect Your Client</CardTitle>
<CardDescription>
Add this server configuration to your MCP client settings (e.g., inside mcpServers section).
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Gateway URL */}
<div className="flex items-center gap-2 text-sm">
<span className={`h-2 w-2 rounded-full ${gatewayStatus.running ? 'bg-green-500' : 'bg-orange-500'}`} />
<span className="text-[rgb(var(--muted))]">Gateway:</span>
<code className="bg-[var(--surface)] px-2 py-1 rounded text-primary-500">
{gatewayStatus.url || 'http://localhost:3100'}
</code>
{!gatewayStatus.running && (
<span className="text-orange-500 text-xs">(not running)</span>
)}
</div>

{/* Config Display */}
<div className="relative">
<pre className="bg-mcpmux-dark text-primary-100 p-4 rounded-lg text-sm overflow-x-auto font-mono">
{`"mcpmux": {
"type": "http",
"url": "${gatewayStatus.url || 'http://localhost:3100'}/mcp"
}`}
</pre>
<Button
variant="secondary"
size="sm"
className="absolute top-2 right-2"
onClick={async () => {
const config = `"mcpmux": {\n "type": "http",\n "url": "${gatewayStatus.url || 'http://localhost:3100'}/mcp"\n}`;
await navigator.clipboard.writeText(config);
setExportSuccess('Config copied to clipboard!');
setTimeout(() => setExportSuccess(null), 2000);
}}
data-testid="copy-config-btn"
>
📋 Copy
</Button>
</div>

{exportSuccess && (
<div className="text-sm text-green-600 dark:text-green-400 flex items-center gap-1">
<Check className="h-4 w-4" />
{exportSuccess}
</div>
)}
</div>
</CardContent>
</Card>
{/* Connect IDEs — one-click install */}
<ConnectIDEs
gatewayUrl={gatewayStatus.url || 'http://localhost:3100'}
gatewayRunning={gatewayStatus.running}
/>
</div>
);
}
Expand Down
Loading
Loading