Commit 9715ab6

mo khan <mo@mokhan.ca>
2026-01-19 20:30:49
feat: add support for connecting to MCP servers
1 parent f254ffb
Changed files (3)
lib/elelem/agent.rb
@@ -12,6 +12,8 @@ module Elelem
       @terminal = terminal || Terminal.new(commands: COMMANDS)
       @history = history || [{ role: "system", content: system_prompt }]
       @toolbox.add("task", task_tool)
+      @mcp = MCP.new
+      @mcp.tools.each { |name, tool| @toolbox.add(name, tool) }
     end
 
     def repl
lib/elelem/mcp.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module Elelem
+  class MCP
+    def initialize(config_path = ".mcp.json")
+      @config = File.exist?(config_path) ? JSON.parse(IO.read(config_path)) : {}
+      @servers = {}
+    end
+
+    def tools
+      @config.fetch("mcpServers", {}).flat_map do |name, _|
+        server(name).tools.map do |tool|
+          [
+            "#{name}_#{tool["name"]}",
+            {
+              desc: tool["description"],
+              params: tool.dig("inputSchema", "properties") || {},
+              required: tool.dig("inputSchema", "required") || [],
+              fn: ->(a) { server(name).call(tool["name"], a) }
+            }
+          ]
+        end
+      end.to_h
+    end
+
+    def close
+      @servers.each_value(&:close)
+    end
+
+    private
+
+    def server(name)
+      @servers[name] ||= Server.new(**@config.dig("mcpServers", name).transform_keys(&:to_sym))
+    end
+
+    class Server
+      def initialize(command:, args: [], env: {})
+        resolved_env = env.transform_values { |v| v.gsub(/\$\{(\w+)\}/) { ENV[$1] } }
+        @stdin, @stdout, @stderr, @wait = Open3.popen3(resolved_env, command, *args)
+        @id = 0
+        initialize!
+      end
+
+      def tools
+        request("tools/list")["tools"]
+      end
+
+      def call(name, args)
+        result = request("tools/call", { name: name, arguments: args })
+        { content: result["content"]&.map { |c| c["text"] }&.join("\n") }
+      end
+
+      def close
+        @stdin.close rescue nil
+        @stdout.close rescue nil
+        @stderr.close rescue nil
+        @wait.kill rescue nil
+      end
+
+      private
+
+      def initialize!
+        request("initialize", {
+          protocolVersion: "2024-11-05",
+          capabilities: {},
+          clientInfo: { name: "elelem", version: VERSION }
+        })
+        notify("notifications/initialized")
+      end
+
+      def request(method, params = {})
+        send_msg(id: @id += 1, method: method, params: params)
+        read_response(@id)
+      end
+
+      def notify(method, params = {})
+        send_msg(method: method, params: params)
+      end
+
+      def send_msg(msg)
+        @stdin.puts({ jsonrpc: "2.0", **msg }.to_json)
+        @stdin.flush
+      end
+
+      def read_response(id)
+        loop do
+          line = @stdout.gets
+          raise "Server closed" unless line
+          msg = JSON.parse(line)
+          return msg["result"] if msg["id"] == id
+          raise msg["error"]["message"] if msg["error"]
+        end
+      end
+    end
+  end
+end
lib/elelem.rb
@@ -10,6 +10,7 @@ require "reline"
 require "stringio"
 
 require_relative "elelem/agent"
+require_relative "elelem/mcp"
 require_relative "elelem/terminal"
 require_relative "elelem/toolbox"
 require_relative "elelem/version"