From ca485b0953d775b5284e9699f68c263a52d69ec8 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 1 Jun 2026 17:55:27 +0100 Subject: [PATCH] Make MCP executable recover on non-FHS Linux --- exe/rubydex_mcp | 15 ++------ lib/rubydex/mcp_server_launcher.rb | 57 ++++++++++++++++++++++++++++ test/mcp_server_executable_test.rb | 60 ++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 lib/rubydex/mcp_server_launcher.rb create mode 100644 test/mcp_server_executable_test.rb diff --git a/exe/rubydex_mcp b/exe/rubydex_mcp index bca33de47..8c1742b52 100755 --- a/exe/rubydex_mcp +++ b/exe/rubydex_mcp @@ -1,17 +1,8 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require "rbconfig" +$LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) -host_os = RbConfig::CONFIG.fetch("host_os") -executable = host_os.match?(/mswin|mingw|cygwin/) ? "rubydex_mcp.exe" : "rubydex_mcp" -binary = File.expand_path("../lib/rubydex/bin/#{executable}", __dir__) +require "rubydex/mcp_server_launcher" -unless File.executable?(binary) - abort(<<~MESSAGE.chomp) - rubydex_mcp is not available at #{binary}. - Install a precompiled rubydex gem, or reinstall rubydex with Cargo available so the MCP executable can be built locally. - MESSAGE -end - -exec(binary, *ARGV) +Rubydex::MCPServerLauncher.exec_server(ARGV) diff --git a/lib/rubydex/mcp_server_launcher.rb b/lib/rubydex/mcp_server_launcher.rb new file mode 100644 index 000000000..f1ab4f161 --- /dev/null +++ b/lib/rubydex/mcp_server_launcher.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "rbconfig" + +module Rubydex + # Launcher-only helpers for `exe/rubydex_mcp`. + # + # Do not require this file from `lib/rubydex.rb`. It exists only so the Ruby executable wrapper can handle + # host-specific process launch details before it execs the Rust MCP server. + module MCPServerLauncher + extend self + + def windows? + RbConfig::CONFIG.fetch("host_os").match?(/mswin|mingw|cygwin/) + end + + def executable_name + windows? ? "rubydex_mcp.exe" : "rubydex_mcp" + end + + def binary_path + File.expand_path("bin/#{executable_name}", __dir__) + end + + def host_loader(maps_path) + File.foreach(maps_path) do |line| + path = line.split.last + next unless path && File.absolute_path?(path) + next unless File.basename(path).match?(/\Ald-.*\.so/) + + return path if File.exist?(path) + end + + nil + rescue Errno::ENOENT + nil + end + + def exec_server(argv) + binary = binary_path + + unless File.executable?(binary) + abort(<<~MESSAGE.chomp) + rubydex_mcp is not available at #{binary}. + Install a precompiled rubydex gem, or reinstall rubydex with Cargo available so the MCP executable can be built locally. + MESSAGE + end + + exec(binary, *argv) + rescue Errno::ENOENT + loader = host_loader("/proc/self/maps") + raise unless loader + + exec(loader, binary, *argv) + end + end +end diff --git a/test/mcp_server_executable_test.rb b/test/mcp_server_executable_test.rb new file mode 100644 index 000000000..478a4a885 --- /dev/null +++ b/test/mcp_server_executable_test.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +require "fileutils" +require "tmpdir" +require "tempfile" +require "rubydex/mcp_server_launcher" + +class MCPServerExecutableTest < Minitest::Test + def test_host_loader_reads_loader_from_maps + dir = Dir.mktmpdir + loader_path = File.join(dir, "ld-linux-x86-64.so.2") + File.write(loader_path, "") + + maps = Tempfile.new("maps") + maps.write(<<~MAPS) + 7f3d10000000-7f3d10022000 r--p 00000000 00:00 0 [heap] + 7f3d20000000-7f3d20022000 r-xp 00000000 00:00 0 #{loader_path} + MAPS + maps.close + + assert_equal(loader_path, Rubydex::MCPServerLauncher.host_loader(maps.path)) + ensure + maps&.unlink + FileUtils.remove_entry_secure(dir) if dir && File.directory?(dir) + end + + def test_host_loader_ignores_missing_loader_paths + Dir.mktmpdir do |dir| + loader_path = File.join(dir, "ld-linux-x86-64.so.2") + File.write(loader_path, "") + + maps = Tempfile.new("maps") + maps.write(<<~MAPS) + 7f3d10000000-7f3d10022000 r-xp 00000000 00:00 0 /missing/ld-linux-x86-64.so.2 + 7f3d20000000-7f3d20022000 r-xp 00000000 00:00 0 #{loader_path} + MAPS + maps.close + + assert_equal(loader_path, Rubydex::MCPServerLauncher.host_loader(maps.path)) + ensure + maps&.unlink + end + end + + def test_host_loader_returns_nil_when_maps_file_is_missing + assert_nil(Rubydex::MCPServerLauncher.host_loader("/missing/proc/self/maps")) + end + + def test_host_loader_returns_existing_linux_loader + skip("Linux-only") unless File.file?("/proc/self/maps") + + loader = Rubydex::MCPServerLauncher.host_loader("/proc/self/maps") + + refute_nil(loader) + assert_path_exists(loader) + assert_match(/\Ald-.*\.so/, File.basename(loader)) + end +end