From 923f7ab3df6eb452c0b722994a99df78c0a3188d Mon Sep 17 00:00:00 2001 From: William Brennan Date: Mon, 12 Jan 2026 02:58:29 +0100 Subject: [PATCH 1/2] fix: resolve Tokio runtime nesting panic in tool handlers Replace direct `runtime.block_on()` calls with `tokio::task::block_in_place()` wrapper in all 12 tool handler methods in McpServerImpl. The previous implementation would panic with "Cannot start a runtime from within a runtime" when any tool was called via tools/call, because the JSON-RPC handlers run inside an async context but the tool methods were trying to block on the current runtime. The fix uses `block_in_place()` which is designed for this exact scenario - it moves the blocking operation to a blocking thread pool while allowing the async runtime to continue processing. Affected methods: - launch_app - stop_app - get_app_logs - take_screenshot - get_window_info - send_keyboard_input - send_mouse_click - execute_js - get_devtools_info - monitor_resources - list_ipc_handlers - call_ipc_command Co-Authored-By: Claude Opus 4.5 --- src/server.rs | 116 ++++++++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 52 deletions(-) diff --git a/src/server.rs b/src/server.rs index c9ffdcc..981b6f2 100644 --- a/src/server.rs +++ b/src/server.rs @@ -501,11 +501,12 @@ impl McpServerImpl { fn launch_app(&self, app_path: String, args: Option>) -> jsonrpc_core::Result { let process_manager = Arc::clone(&self.process_manager); let args = args.unwrap_or_default(); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - let mut manager = process_manager.write().await; - manager.launch_app(&app_path, args).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let mut manager = process_manager.write().await; + manager.launch_app(&app_path, args).await + }) }); match result { @@ -519,11 +520,12 @@ impl McpServerImpl { fn stop_app(&self, process_id: String) -> jsonrpc_core::Result { let process_manager = Arc::clone(&self.process_manager); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - let mut manager = process_manager.write().await; - manager.stop_app(&process_id).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let mut manager = process_manager.write().await; + manager.stop_app(&process_id).await + }) }); match result { @@ -536,11 +538,12 @@ impl McpServerImpl { fn get_app_logs(&self, process_id: String, lines: Option) -> jsonrpc_core::Result { let process_manager = Arc::clone(&self.process_manager); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - let manager = process_manager.read().await; - manager.get_app_logs(&process_id, lines).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let manager = process_manager.read().await; + manager.get_app_logs(&process_id, lines).await + }) }); match result { @@ -554,10 +557,11 @@ impl McpServerImpl { fn take_screenshot(&self, process_id: String, output_path: Option) -> jsonrpc_core::Result { let window_manager = Arc::clone(&self.window_manager); let output_path = output_path.map(PathBuf::from); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - window_manager.take_screenshot(&process_id, output_path).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + window_manager.take_screenshot(&process_id, output_path).await + }) }); match result { @@ -570,10 +574,11 @@ impl McpServerImpl { fn get_window_info(&self, process_id: String) -> jsonrpc_core::Result { let window_manager = Arc::clone(&self.window_manager); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - window_manager.get_window_info(&process_id).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + window_manager.get_window_info(&process_id).await + }) }); match result { @@ -584,10 +589,11 @@ impl McpServerImpl { fn send_keyboard_input(&self, process_id: String, keys: String) -> jsonrpc_core::Result { let input_simulator = Arc::clone(&self.input_simulator); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - input_simulator.send_keyboard_input(&process_id, &keys).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + input_simulator.send_keyboard_input(&process_id, &keys).await + }) }); match result { @@ -601,10 +607,11 @@ impl McpServerImpl { fn send_mouse_click(&self, process_id: String, x: i32, y: i32, button: Option) -> jsonrpc_core::Result { let input_simulator = Arc::clone(&self.input_simulator); let button = button.unwrap_or_else(|| "left".to_string()); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - input_simulator.send_mouse_click(&process_id, x, y, &button).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + input_simulator.send_mouse_click(&process_id, x, y, &button).await + }) }); match result { @@ -617,10 +624,11 @@ impl McpServerImpl { fn execute_js(&self, process_id: String, javascript_code: String) -> jsonrpc_core::Result { let debug_tools = Arc::clone(&self.debug_tools); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - debug_tools.execute_js(&process_id, &javascript_code).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + debug_tools.execute_js(&process_id, &javascript_code).await + }) }); match result { @@ -633,10 +641,11 @@ impl McpServerImpl { fn get_devtools_info(&self, process_id: String) -> jsonrpc_core::Result { let debug_tools = Arc::clone(&self.debug_tools); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - debug_tools.get_devtools_info(&process_id).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + debug_tools.get_devtools_info(&process_id).await + }) }); match result { @@ -647,11 +656,12 @@ impl McpServerImpl { fn monitor_resources(&self, process_id: String) -> jsonrpc_core::Result { let process_manager = Arc::clone(&self.process_manager); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - let manager = process_manager.read().await; - manager.monitor_resources(&process_id).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let manager = process_manager.read().await; + manager.monitor_resources(&process_id).await + }) }); match result { @@ -662,10 +672,11 @@ impl McpServerImpl { fn list_ipc_handlers(&self, process_id: String) -> jsonrpc_core::Result { let ipc_manager = Arc::clone(&self.ipc_manager); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - ipc_manager.list_ipc_handlers(&process_id).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + ipc_manager.list_ipc_handlers(&process_id).await + }) }); match result { @@ -679,10 +690,11 @@ impl McpServerImpl { fn call_ipc_command(&self, process_id: String, command_name: String, args: Option) -> jsonrpc_core::Result { let ipc_manager = Arc::clone(&self.ipc_manager); let args = args.unwrap_or(Value::Null); - - let runtime = tokio::runtime::Handle::current(); - let result = runtime.block_on(async { - ipc_manager.call_ipc_command(&process_id, &command_name, args).await + + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + ipc_manager.call_ipc_command(&process_id, &command_name, args).await + }) }); match result { From 16aae517a1b6d88d24fc1dfbedafbd61cc26bad9 Mon Sep 17 00:00:00 2001 From: William Brennan Date: Mon, 12 Jan 2026 10:47:11 +0100 Subject: [PATCH 2/2] fix: use empty objects instead of null in capabilities response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code's MCP client fails to connect when capabilities fields (resources, prompts, logging) are null. Other working MCP servers like narsil-mcp use empty objects {} instead. This fix changes the initialize response from: "resources": null, "prompts": null, "logging": null To: "resources": {}, "prompts": {}, "logging": {} Tested with `claude mcp list` - server now shows "✓ Connected". Co-Authored-By: Claude Opus 4.5 --- src/server.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server.rs b/src/server.rs index 981b6f2..0dfd854 100644 --- a/src/server.rs +++ b/src/server.rs @@ -484,9 +484,9 @@ impl McpServerImpl { "tools": { "listTools": true }, - "resources": null, - "prompts": null, - "logging": null + "resources": {}, + "prompts": {}, + "logging": {} } })) }