Skip to content

Commit 8b386fc

Browse files
committed
Make MCP executable recover on non-FHS Linux
1 parent 33e510e commit 8b386fc

3 files changed

Lines changed: 96 additions & 12 deletions

File tree

exe/rubydex_mcp

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
11
#!/usr/bin/env ruby
22
# frozen_string_literal: true
33

4-
require "rbconfig"
4+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
55

6-
host_os = RbConfig::CONFIG.fetch("host_os")
7-
executable = host_os.match?(/mswin|mingw|cygwin/) ? "rubydex_mcp.exe" : "rubydex_mcp"
8-
binary = File.expand_path("../lib/rubydex/bin/#{executable}", __dir__)
6+
require "rubydex/mcp_server_launcher"
97

10-
unless File.executable?(binary)
11-
abort(<<~MESSAGE.chomp)
12-
rubydex_mcp is not available at #{binary}.
13-
Install a precompiled rubydex gem, or reinstall rubydex with Cargo available so the MCP executable can be built locally.
14-
MESSAGE
15-
end
16-
17-
exec(binary, *ARGV)
8+
Rubydex::MCPServerLauncher.exec_server(ARGV)

lib/rubydex/mcp_server_launcher.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
require "rbconfig"
4+
5+
module Rubydex
6+
# Launcher-only helpers for `exe/rubydex_mcp`.
7+
#
8+
# Do not require this file from `lib/rubydex.rb`. It exists only so the Ruby executable wrapper can handle
9+
# host-specific process launch details before it execs the Rust MCP server.
10+
module MCPServerLauncher
11+
extend self
12+
13+
def windows?
14+
RbConfig::CONFIG.fetch("host_os").match?(/mswin|mingw|cygwin/)
15+
end
16+
17+
def executable_name
18+
windows? ? "rubydex_mcp.exe" : "rubydex_mcp"
19+
end
20+
21+
def binary_path
22+
File.expand_path("bin/#{executable_name}", __dir__)
23+
end
24+
25+
def host_loader(maps_path = "/proc/self/maps")
26+
File.foreach(maps_path) do |line|
27+
path = line.split.last
28+
next unless path&.start_with?("/")
29+
30+
return path if File.basename(path).match?(/\Ald-.*\.so/)
31+
end
32+
33+
nil
34+
rescue Errno::ENOENT
35+
nil
36+
end
37+
38+
def exec_server(argv)
39+
binary = binary_path
40+
41+
unless File.executable?(binary)
42+
abort(<<~MESSAGE.chomp)
43+
rubydex_mcp is not available at #{binary}.
44+
Install a precompiled rubydex gem, or reinstall rubydex with Cargo available so the MCP executable can be built locally.
45+
MESSAGE
46+
end
47+
48+
exec(binary, *argv)
49+
rescue Errno::ENOENT
50+
raise if windows?
51+
52+
loader = host_loader
53+
raise unless loader && File.exist?(loader)
54+
55+
exec(loader, binary, *argv)
56+
end
57+
end
58+
end

test/mcp_server_executable_test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "test_helper"
4+
5+
require "tempfile"
6+
require "rubydex/mcp_server_launcher"
7+
8+
class MCPServerExecutableTest < Minitest::Test
9+
def test_host_loader_reads_loader_from_maps
10+
maps = Tempfile.new("maps")
11+
maps.write(<<~MAPS)
12+
7f3d10000000-7f3d10022000 r--p 00000000 00:00 0 [heap]
13+
7f3d20000000-7f3d20022000 r-xp 00000000 00:00 0 /nix/store/glibc/lib/ld-linux-x86-64.so.2
14+
MAPS
15+
maps.close
16+
17+
assert_equal("/nix/store/glibc/lib/ld-linux-x86-64.so.2", Rubydex::MCPServerLauncher.host_loader(maps.path))
18+
ensure
19+
maps&.unlink
20+
end
21+
22+
def test_host_loader_returns_nil_when_maps_file_is_missing
23+
assert_nil(Rubydex::MCPServerLauncher.host_loader("/missing/proc/self/maps"))
24+
end
25+
26+
def test_host_loader_returns_existing_linux_loader
27+
skip("Linux-only") unless File.file?("/proc/self/maps")
28+
29+
loader = Rubydex::MCPServerLauncher.host_loader
30+
31+
refute_nil(loader)
32+
assert_path_exists(loader)
33+
assert_match(/\Ald-.*\.so/, File.basename(loader))
34+
end
35+
end

0 commit comments

Comments
 (0)