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
87 changes: 79 additions & 8 deletions src-tauri/src/commands/mcp.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use chrono::Utc;
use dirs;
use log::{error, info};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -612,7 +613,10 @@ pub async fn mcp_add_from_claude_desktop(

/// Starts Claude Code as an MCP server
#[tauri::command]
pub async fn mcp_serve(app: AppHandle) -> Result<String, String> {
pub async fn mcp_serve(
app: AppHandle,
registry: tauri::State<'_, crate::process::ProcessRegistryState>,
) -> Result<String, String> {
info!("Starting Claude Code as MCP server");

// Start the server in a separate process
Expand All @@ -628,9 +632,41 @@ pub async fn mcp_serve(app: AppHandle) -> Result<String, String> {
cmd.arg("mcp").arg("serve");

match cmd.spawn() {
Ok(_) => {
info!("Successfully started Claude Code MCP server");
Ok("Claude Code MCP server started".to_string())
Ok(child) => {
let pid = child.id();
if pid == 0 {
error!("MCP server started but PID is unavailable");
return Err("MCP server started but PID is unavailable".to_string());
}

// Intentionally drop the child handle - MCP server runs as detached process.
// We track it by PID only, allowing it to outlive the parent process.
std::mem::forget(child);

// Register atomically - will fail if another instance already exists
match registry.0.register_mcp_serve_process(pid) {
Ok(_run_id) => {
info!("Successfully started Claude Code MCP server (PID: {})", pid);
Ok(format!("Claude Code MCP server started (PID: {})", pid))
}
Err(e) => {
// Registration failed (likely already running) - kill the process we just started
error!("Failed to register MCP server process: {}", e);
#[cfg(unix)]
{
use std::process::Command;
let _ = Command::new("kill").arg(pid.to_string()).spawn();
}
#[cfg(windows)]
{
use std::process::Command;
let _ = Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/F"])
.spawn();
}
Err(e)
}
}
}
Err(e) => {
error!("Failed to start MCP server: {}", e);
Expand All @@ -639,6 +675,25 @@ pub async fn mcp_serve(app: AppHandle) -> Result<String, String> {
}
}

/// Stops Claude Code MCP server if running
#[tauri::command]
pub async fn mcp_stop(
registry: tauri::State<'_, crate::process::ProcessRegistryState>,
) -> Result<String, String> {
if let Ok(Some(proc_info)) = registry.0.get_running_mcp_serve() {
let run_id = proc_info.run_id;
let pid = proc_info.pid;
registry
.0
.kill_process(run_id)
.await
.map_err(|e| format!("Failed to stop MCP server (PID: {}): {}", pid, e))?;
Ok(format!("Claude Code MCP server stopped (PID: {})", pid))
} else {
Ok("Claude Code MCP server is not running".to_string())
}
}

/// Tests connection to an MCP server
#[tauri::command]
pub async fn mcp_test_connection(app: AppHandle, name: String) -> Result<String, String> {
Expand Down Expand Up @@ -670,12 +725,28 @@ pub async fn mcp_reset_project_choices(app: AppHandle) -> Result<String, String>

/// Gets the status of MCP servers
#[tauri::command]
pub async fn mcp_get_server_status() -> Result<HashMap<String, ServerStatus>, String> {
pub async fn mcp_get_server_status(
registry: tauri::State<'_, crate::process::ProcessRegistryState>,
) -> Result<HashMap<String, ServerStatus>, String> {
info!("Getting MCP server status");

// TODO: Implement actual status checking
// For now, return empty status
Ok(HashMap::new())
let mut status_map = HashMap::new();

if let Ok(Some(proc_info)) = registry.0.get_running_mcp_serve() {
status_map.insert(
"claude-code".to_string(),
ServerStatus {
running: true,
error: None,
last_checked: Some(Utc::now().timestamp() as u64),
},
);

// Also include PID in the log for debugging
info!("MCP serve running with PID: {}", proc_info.pid);
}

Ok(status_map)
}

/// Reads .mcp.json from the current project
Expand Down
3 changes: 2 additions & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use commands::claude::{
use commands::mcp::{
mcp_add, mcp_add_from_claude_desktop, mcp_add_json, mcp_get, mcp_get_server_status, mcp_list,
mcp_read_project_config, mcp_remove, mcp_reset_project_choices, mcp_save_project_config,
mcp_serve, mcp_test_connection,
mcp_serve, mcp_stop, mcp_test_connection,
};

use commands::proxy::{apply_proxy_settings, get_proxy_settings, save_proxy_settings};
Expand Down Expand Up @@ -268,6 +268,7 @@ fn main() {
mcp_add_json,
mcp_add_from_claude_desktop,
mcp_serve,
mcp_stop,
mcp_test_connection,
mcp_reset_project_choices,
mcp_get_server_status,
Expand Down
54 changes: 54 additions & 0 deletions src-tauri/src/process/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use tokio::process::Child;
pub enum ProcessType {
AgentRun { agent_id: i64, agent_name: String },
ClaudeSession { session_id: String },
McpServe,
}

/// Information about a running agent process
Expand Down Expand Up @@ -82,6 +83,7 @@ impl ProcessRegistry {
}

/// Register a new running agent process using sidecar (similar to register_process but for sidecar children)
#[allow(dead_code)]
pub fn register_sidecar_process(
&self,
run_id: i64,
Expand Down Expand Up @@ -152,6 +154,58 @@ impl ProcessRegistry {
Ok(run_id)
}

/// Register a long-running MCP serve process (stores PID only, no child handle)
///
/// NOTE: Only ONE MCP serve process should run at a time (singleton pattern).
/// This method enforces the singleton by checking atomically within the lock.
pub fn register_mcp_serve_process(&self, pid: u32) -> Result<i64, String> {
// Acquire lock first to make check-and-register atomic (prevents race conditions)
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;

// Check if MCP serve process already exists (atomic with registration)
let existing_mcp = processes.values().find(|p| {
matches!(p.info.process_type, ProcessType::McpServe)
});

if let Some(existing) = existing_mcp {
return Err(format!(
"MCP server already running (PID: {})",
existing.info.pid
));
}

// Now safe to register new MCP serve process
let run_id = self.generate_id()?;

let process_info = ProcessInfo {
run_id,
process_type: ProcessType::McpServe,
pid,
started_at: Utc::now(),
project_path: "".to_string(),
task: "claude mcp serve".to_string(),
model: "".to_string(),
};

let process_handle = ProcessHandle {
info: process_info,
child: Arc::new(Mutex::new(None)),
live_output: Arc::new(Mutex::new(String::new())),
};

processes.insert(run_id, process_handle);
Ok(run_id)
}

/// Get the currently running MCP serve process if any
pub fn get_running_mcp_serve(&self) -> Result<Option<ProcessInfo>, String> {
let processes = self.processes.lock().map_err(|e| e.to_string())?;
Ok(processes
.values()
.find(|handle| matches!(handle.info.process_type, ProcessType::McpServe))
.map(|handle| handle.info.clone()))
}

/// Internal method to register any process
fn register_process_internal(
&self,
Expand Down
100 changes: 78 additions & 22 deletions src/components/MCPImportExport.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { Download, Upload, FileText, Loader2, Info, Network, Settings2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
Expand All @@ -15,6 +15,10 @@ interface MCPImportExportProps {
* Callback for error messages
*/
onError: (message: string) => void;
/**
* Callback for success/info messages
*/
onSuccess: (message: string) => void;
}

/**
Expand All @@ -23,11 +27,31 @@ interface MCPImportExportProps {
export const MCPImportExport: React.FC<MCPImportExportProps> = ({
onImportCompleted,
onError,
onSuccess,
}) => {
const [importingDesktop, setImportingDesktop] = useState(false);
const [importingJson, setImportingJson] = useState(false);
const [importScope, setImportScope] = useState("local");

const [mcpServeRunning, setMcpServeRunning] = useState(false);
const [mcpServeChecking, setMcpServeChecking] = useState(false);

const refreshMcpServeStatus = async () => {
try {
const statuses = await api.mcpGetServerStatus();
setMcpServeRunning(Boolean(statuses["claude-code"]?.running));
} catch {
// If status fails, don't block UX; just assume stopped
setMcpServeRunning(false);
}
};

useEffect(() => {
refreshMcpServeStatus();
const handle = window.setInterval(refreshMcpServeStatus, 2000);
return () => window.clearInterval(handle);
}, []);

/**
* Imports servers from Claude Desktop
*/
Expand All @@ -39,23 +63,18 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({

// Show detailed results if available
if (result.servers && result.servers.length > 0) {
const successfulServers = result.servers.filter(s => s.success);
const failedServers = result.servers.filter(s => !s.success);

if (successfulServers.length > 0) {
const successMessage = `Successfully imported: ${successfulServers.map(s => s.name).join(", ")}`;
onImportCompleted(result.imported_count, result.failed_count);
// Show success details
if (failedServers.length === 0) {
onError(successMessage);
}
}
// Always call onImportCompleted for server list refresh and count-based toast
onImportCompleted(result.imported_count, result.failed_count);

// Only show detailed error messages for failed servers (onImportCompleted already shows success)
if (failedServers.length > 0) {
const failureDetails = failedServers
.map(s => `${s.name}: ${s.error || "Unknown error"}`)
.join("\n");
onError(`Failed to import some servers:\n${failureDetails}`);
console.warn("Failed to import some servers:", failureDetails);
// Don't call onError here - onImportCompleted already handles the toast
}
} else {
onImportCompleted(result.imported_count, result.failed_count);
Expand Down Expand Up @@ -152,11 +171,29 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
*/
const handleStartMCPServer = async () => {
try {
await api.mcpServe();
onError("Claude Code MCP server started. You can now connect to it from other applications.");
setMcpServeChecking(true);
const message = await api.mcpServe();
await refreshMcpServeStatus();
onSuccess(message);
} catch (error) {
console.error("Failed to start MCP server:", error);
onError("Failed to start Claude Code as MCP server");
} finally {
setMcpServeChecking(false);
}
};

const handleStopMCPServer = async () => {
try {
setMcpServeChecking(true);
const message = await api.mcpStop();
await refreshMcpServeStatus();
onSuccess(message);
} catch (error) {
console.error("Failed to stop MCP server:", error);
onError("Failed to stop Claude Code MCP server");
} finally {
setMcpServeChecking(false);
}
};

Expand Down Expand Up @@ -305,20 +342,39 @@ export const MCPImportExport: React.FC<MCPImportExportProps> = ({
<Network className="h-5 w-5 text-green-500" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium">Use Claude Code as MCP Server</h4>
<div className="flex items-center justify-between gap-3">
<h4 className="text-sm font-medium">Use Claude Code as MCP Server</h4>
<div className="text-xs text-muted-foreground">
{mcpServeChecking ? "Checking…" : mcpServeRunning ? "Running" : "Stopped"}
</div>
</div>
<p className="text-xs text-muted-foreground mt-1">
Start Claude Code as an MCP server that other applications can connect to
</p>
</div>
</div>
<Button
onClick={handleStartMCPServer}
variant="outline"
className="w-full gap-2 border-green-500/20 hover:bg-green-500/10 hover:text-green-600 hover:border-green-500/50"
>
<Network className="h-4 w-4" />
Start MCP Server
</Button>

{mcpServeRunning ? (
<Button
onClick={handleStopMCPServer}
variant="outline"
className="w-full gap-2 border-red-500/20 hover:bg-red-500/10 hover:text-red-600 hover:border-red-500/50"
disabled={mcpServeChecking}
>
<Network className="h-4 w-4" />
Stop MCP Server
</Button>
) : (
<Button
onClick={handleStartMCPServer}
variant="outline"
className="w-full gap-2 border-green-500/20 hover:bg-green-500/10 hover:text-green-600 hover:border-green-500/50"
disabled={mcpServeChecking}
>
<Network className="h-4 w-4" />
Start MCP Server
</Button>
)}
</div>
</Card>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/MCPManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,10 @@ export const MCPManager: React.FC<MCPManagerProps> = ({
{/* Import/Export Tab */}
<TabsContent value="import" className="space-y-6 mt-6">
<Card className="overflow-hidden">
<MCPImportExport
<MCPImportExport
onImportCompleted={handleImportCompleted}
onError={(message: string) => setToast({ message, type: "error" })}
onSuccess={(message: string) => setToast({ message, type: "success" })}
/>
</Card>
</TabsContent>
Expand Down
Loading