Commit ea1bbc2

mo khan <mo@mokhan.ca>
2025-08-14 01:04:06
fix: prevent infinite loop and add better mcp handling
1 parent fafb5a6
Changed files (3)
lib/elelem/mcp_client.rb
@@ -6,7 +6,7 @@ module Elelem
 
     def initialize(configuration, command = [])
       @configuration = configuration
-      @stdin, @stdout, @stderr, @worker_thread = Open3.popen3(*command, pgroup: true)
+      @stdin, @stdout, @stderr, @worker = Open3.popen3(*command, pgroup: true)
 
       # 1. Send initialize request
       send_request(
@@ -31,7 +31,16 @@ module Elelem
     end
 
     def connected?
-      @worker_thread&.alive? && @stdin && !@stdin.closed?
+      return false unless @worker&.alive?
+      return false unless @stdin && !@stdin.closed?
+      return false unless @stdout && !@stdout.closed?
+
+      begin
+        Process.getpgid(@worker.pid)
+        true
+      rescue Errno::ESRCH
+        false
+      end
     end
 
     def call(name, arguments = {})
@@ -46,9 +55,11 @@ module Elelem
 
     private
 
-    attr_reader :stdin, :stdout, :stderr, :worker_thread, :configuration
+    attr_reader :stdin, :stdout, :stderr, :worker, :configuration
 
     def send_request(method:, params: {})
+      return {} unless connected?
+
       request = {
         jsonrpc: "2.0",
         id: Time.now.to_i,
@@ -56,14 +67,19 @@ module Elelem
       }
       request[:params] = params unless params.empty?
       configuration.logger.debug(JSON.pretty_generate(request))
+
       @stdin.puts(JSON.generate(request))
       @stdin.flush
 
-      response = JSON.parse(@stdout.gets.strip)
+      response_line = @stdout.gets&.strip
+      return {} if response_line.nil? || response_line.empty?
+
+      response = JSON.parse(response_line)
       configuration.logger.debug(JSON.pretty_generate(response))
+
       if response["error"]
         configuration.logger.error(response["error"]["message"])
-        {}
+        { error: response["error"]["message"] }
       else
         response["result"]
       end
lib/elelem/state.rb
@@ -71,7 +71,15 @@ module Elelem
             agent.say("\n\n", newline: false)
 
             result = agent.execute(tool_call)
-            agent.conversation.add(role: :tool, content: result)
+
+            if result.is_a?(Hash) && result[:success] == false
+              agent.say("\n", newline: false)
+              agent.complete_progress("#{tool_name} failed", colour: :red)
+              return Error.new(agent, result[:error])
+            end
+
+            output = result.is_a?(Hash) ? result[:output] : result
+            agent.conversation.add(role: :tool, content: output)
 
             agent.say("\n", newline: false)
             agent.complete_progress("#{tool_name} completed")
@@ -82,6 +90,19 @@ module Elelem
       end
     end
 
+    class Error < State
+      def initialize(agent, error_message)
+        super(agent)
+        @error_message = error_message
+      end
+
+      def process(_message)
+        agent.say("\nTool execution failed: #{@error_message}", colour: :red)
+        agent.say("Returning to idle state.\n\n", colour: :yellow)
+        Waiting.new(agent)
+      end
+    end
+
     class Talking < State
       def initialize(agent)
         super(agent)
lib/elelem/tool.rb
@@ -90,10 +90,29 @@ module Elelem
     end
 
     def call(args)
+      unless client.connected?
+        error_msg = "MCP connection lost"
+        tui.say(error_msg, colour: :red)
+        return { success: false, output: "", error: error_msg }
+      end
+
       result = client.call(name, args)
+
+      if result.nil? || result.empty?
+        error_msg = "Tool call failed: no response from MCP server"
+        tui.say(error_msg, colour: :red)
+        return { success: false, output: "", error: error_msg }
+      end
+
+      if result["error"]
+        error_msg = "Tool error: #{result["error"]}"
+        tui.say(error_msg, colour: :red)
+        return { success: false, output: "", error: error_msg }
+      end
+
       output = result.dig("content", 0, "text") || result.to_s
       tui.say(output)
-      output
+      { success: true, output: output, error: nil }
     end
   end
 end