Commit ebfc50c
Changed files (5)
lib
lib/elelem/configuration.rb
@@ -41,7 +41,7 @@ module Elelem
end
def tools
- @tools ||= Tools.new(self)
+ @tools ||= Tools.new(self, [BashTool.new(self)] + mcp_tools)
end
private
@@ -49,5 +49,9 @@ module Elelem
def scheme
host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
end
+
+ def mcp_tools(clients = [MCPClient.new(self)])
+ @mcp_tools ||= clients.map { |client| client.tools.map { |tool| MCPTool.new(client, tui, tool) } }.flatten
+ end
end
end
lib/elelem/mcp_client.rb
@@ -34,7 +34,7 @@ module Elelem
@worker_thread&.alive? && @stdin && !@stdin.closed?
end
- def call_tool(name, arguments = {})
+ def call(name, arguments = {})
send_request(
method: "tools/call",
params: {
lib/elelem/tool.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Elelem
+ class Tool
+ attr_reader :name, :description, :parameters
+
+ def initialize(name, description, parameters)
+ @name = name
+ @description = description
+ @parameters = parameters
+ end
+
+ def banner
+ [name, description].join(": ")
+ end
+
+ def to_h
+ {
+ type: "function",
+ function: {
+ name: name,
+ description: description,
+ parameters: parameters
+ }
+ }
+ end
+ end
+
+ class BashTool < Tool
+ attr_reader :tui
+
+ def initialize(tui)
+ @tui = tui
+ super("bash", "Execute a shell command.", {
+ parameters: {
+ type: "object",
+ properties: {
+ command: { type: "string" }
+ },
+ required: ["command"]
+ }
+ })
+ end
+
+ def call(args)
+ command = args["command"]
+ output_buffer = []
+
+ Open3.popen3("/bin/sh", "-c", command) do |stdin, stdout, stderr, wait_thread|
+ stdin.close
+ streams = [stdout, stderr]
+
+ until streams.empty?
+ ready = IO.select(streams, nil, nil, 0.1)
+
+ if ready
+ ready[0].each do |io|
+ data = io.read_nonblock(4096)
+ output_buffer << data
+
+ if io == stderr
+ tui.say(data, colour: :red, newline: false)
+ else
+ tui.say(data, newline: false)
+ end
+ rescue IO::WaitReadable
+ next
+ rescue EOFError
+ streams.delete(io)
+ end
+ elsif !wait_thread.alive?
+ break
+ end
+ end
+
+ wait_thread.value
+ end
+
+ output_buffer.join
+ end
+ end
+
+ class MCPTool < Tool
+ attr_reader :client, :tui
+
+ def initialize(client, tui, tool)
+ @client = client
+ @tui = tui
+ super(tool["name"], tool["description"], tool["inputSchema"] || {})
+ end
+
+ def call(args)
+ result = client.call(name, args)
+ output = result.dig("content", 0, "text") || result.to_s
+ tui.say(output)
+ output
+ end
+ end
+end
lib/elelem/tools.rb
@@ -2,79 +2,24 @@
module Elelem
class Tools
- DEFAULT_TOOLS = [
- {
- type: "function",
- function: {
- name: "bash",
- description: "Execute a shell command.",
- parameters: {
- type: "object",
- properties: {
- command: { type: "string" }
- },
- required: ["command"]
- }
- },
- handler: lambda { |args|
- stdout, stderr, _status = Open3.capture3("/bin/sh", "-c", args["command"])
- stdout + stderr
- }
- }
- ].freeze
-
- def initialize(configuration, tools = DEFAULT_TOOLS)
+ def initialize(configuration, tools)
@configuration = configuration
- client = MCPClient.new(configuration)
- @tools = tools + client.tools.map do |tool|
- configuration.logger.debug(tool)
- {
- type: "function",
- function: {
- name: tool["name"],
- description: tool["description"],
- parameters: tool["inputSchema"] || {}
- },
- handler: lambda { |args|
- result = client.call_tool(tool["name"], args)
- output = result.dig("content", 0, "text") || result.to_s
- configuration.tui.say(output)
- output
- }
- }
- end
+ @tools = tools
end
def banner
- tools.map do |h|
- [
- h.dig(:function, :name),
- h.dig(:function, :description)
- ].join(": ")
- end.sort.join("\n ")
+ tools.map(&:banner).sort.join("\n ")
end
def execute(tool_call)
name = tool_call.dig("function", "name")
args = tool_call.dig("function", "arguments")
- tool = tools.find { |tool| tool.dig(:function, :name) == name }
- tool.fetch(:handler).call(args).tap do |result|
- configuration.tui.say(result)
- end
+ tools.find { |tool| tool.name == name }&.call(args)
end
def to_h
- tools.map do |tool|
- {
- type: tool[:type],
- function: {
- name: tool.dig(:function, :name),
- description: tool.dig(:function, :description),
- parameters: tool.dig(:function, :parameters)
- }
- }
- end
+ tools.map(&:to_h)
end
private
lib/elelem.rb
@@ -4,7 +4,6 @@ require "json"
require "logger"
require "net/http"
require "open3"
-require "securerandom"
require "thor"
require "uri"
@@ -15,6 +14,7 @@ require_relative "elelem/configuration"
require_relative "elelem/conversation"
require_relative "elelem/mcp_client"
require_relative "elelem/state"
+require_relative "elelem/tool"
require_relative "elelem/tools"
require_relative "elelem/tui"
require_relative "elelem/version"