Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
74 changes: 66 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 @@ -624,13 +628,32 @@ pub async fn mcp_serve(app: AppHandle) -> Result<String, String> {
}
};

// If already running, don't start another one
if let Ok(Some(existing)) = registry.0.get_running_mcp_serve() {
return Ok(format!(
"Claude Code MCP server already running (PID: {})",
existing.pid
));
}

let mut cmd = create_command_with_env(&claude_path);
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());
}

if let Err(e) = registry.0.register_mcp_serve_process(pid) {
error!("Failed to register MCP server process: {}", e);
return Err(e);
}

info!("Successfully started Claude Code MCP server (PID: {})", pid);
Ok(format!("Claude Code MCP server started (PID: {})", pid))
}
Err(e) => {
error!("Failed to start MCP server: {}", e);
Expand All @@ -639,6 +662,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 +712,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
42 changes: 42 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,46 @@ 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).
/// The caller (mcp_serve command) is responsible for checking if a process
/// already exists via get_running_mcp_serve() before calling this method.
pub fn register_mcp_serve_process(&self, pid: u32) -> Result<i64, String> {
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(),
};

// Register without child handle (like sidecar)
let mut processes = self.processes.lock().map_err(|e| e.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
12 changes: 12 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,18 @@ export const api = {
}
},

/**
* Stops Claude Code MCP server
*/
async mcpStop(): Promise<string> {
try {
return await apiCall<string>("mcp_stop");
} catch (error) {
console.error("Failed to stop MCP server:", error);
throw error;
}
},

/**
* Tests connection to an MCP server
*/
Expand Down
Loading