Skip to content

Commit f5f2219

Browse files
committed
Add an MCP server for more efficient agents
1 parent 0399342 commit f5f2219

1 file changed

Lines changed: 188 additions & 0 deletions

File tree

bin/mcp

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# This script is a server that can be run on your local machine, typically given
5+
# to an LLM to reduce token usage and standardize interactions. It listens for
6+
# JSON-RPC requests on stdin, processes them using the defined tools, and writes
7+
# responses to stdout.
8+
#
9+
# To test out the server locally, it is easiest to pipe requests directly into
10+
# stdin and then format JSON responses with a tool like `jq` for readability.
11+
# Here are some example requests you can make to the server:
12+
#
13+
# echo '{"jsonrpc":"2.0","id":"1","method":"ping"}' | bin/mcp | jq
14+
# echo '{"jsonrpc":"2.0","id":"2","method":"tools/list"}' | bin/mcp | jq
15+
# echo '{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"rake","arguments":{"target":"compile"}}}' | bin/mcp | jq
16+
# echo '{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"c_function","arguments":{"filepath":"src/prism.c","function_name":"pm_parser_init"}}}' | bin/mcp | jq
17+
# echo '{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"ruby_method","arguments":{"filepath":"lib/prism.rb","method_name":"load"}}}' | bin/mcp | jq
18+
#
19+
20+
def silence_output
21+
original_stdout = $stdout.dup
22+
original_stderr = $stderr.dup
23+
24+
File.open(File::NULL, "w") do |null|
25+
$stdout.reopen(null)
26+
$stderr.reopen(null)
27+
yield
28+
end
29+
ensure
30+
$stdout.reopen(original_stdout)
31+
$stderr.reopen(original_stderr)
32+
end
33+
34+
require "bundler/inline"
35+
gemfile do
36+
source "https://rubygems.org"
37+
gem "mcp"
38+
gem "open3"
39+
gem "prism", path: File.expand_path("..", __dir__)
40+
41+
silence_output do
42+
gem "ffi-clang"
43+
require "ffi/clang"
44+
end
45+
end
46+
47+
class RakeTool < MCP::Tool
48+
tool_name "rake"
49+
description "Run rake on a specific target."
50+
51+
input_schema(
52+
properties: {
53+
target: { type: "string", enum: ["clean", "compile", "test"], description: "The rake target to build." }
54+
},
55+
required: ["target"]
56+
)
57+
output_schema(
58+
properties: {
59+
success: { type: "boolean" },
60+
stderr: { type: "string" }
61+
},
62+
required: ["success"]
63+
)
64+
65+
def self.call(server_context:, target:)
66+
_, stderr, status =
67+
Dir.chdir(File.expand_path("..", __dir__)) do
68+
Open3.capture3("bundle", "exec", "rake", target)
69+
end
70+
71+
result =
72+
if status.success?
73+
{ success: true }
74+
else
75+
{ success: false, stderr: stderr.strip }
76+
end
77+
78+
output_schema.validate_result(result)
79+
MCP::Tool::Response.new([{ type: "text", text: result.to_json }], structured_content: result)
80+
end
81+
end
82+
83+
class CFunctionTool < MCP::Tool
84+
tool_name "c_function"
85+
description "Extracts a C function from a source file."
86+
87+
input_schema(
88+
properties: {
89+
filepath: { type: "string", description: "Path to the C source file." },
90+
function_name: { type: "string", description: "The name of the function to extract." }
91+
},
92+
required: ["filepath", "function_name"]
93+
)
94+
output_schema(
95+
properties: {
96+
body: { type: "string" }
97+
},
98+
required: ["body"]
99+
)
100+
101+
class << self
102+
def call(server_context:, filepath:, function_name:)
103+
if !File.readable?(filepath)
104+
MCP::Tool::Response.new([MCP::Content::Text.new("Invalid filepath").to_h], error: true)
105+
elsif !(body = find(filepath, function_name))
106+
MCP::Tool::Response.new([MCP::Content::Text.new("Function not found").to_h], error: true)
107+
else
108+
result = { body: body }
109+
output_schema.validate_result(result)
110+
111+
MCP::Tool::Response.new([{ type: "text", text: result.to_json }], structured_content: result)
112+
end
113+
end
114+
115+
private
116+
117+
def extract(cursor)
118+
location = cursor.definition.location
119+
return unless (filepath = location.file)
120+
121+
extent = cursor.extent
122+
start_offset = extent.start.offset
123+
File.open(filepath, "rb") do |file|
124+
file.pread(extent.end.offset - start_offset, start_offset)
125+
end
126+
end
127+
128+
def find(filepath, function_name)
129+
translation_unit = FFI::Clang::Index.new.parse_translation_unit(filepath)
130+
translation_unit.cursor.find { |cursor, _| break extract(cursor) if cursor.declaration? && cursor.spelling == function_name }
131+
end
132+
end
133+
end
134+
135+
class RubyMethodTool < MCP::Tool
136+
tool_name "ruby_method"
137+
description "Extracts a Ruby method from a source file."
138+
139+
input_schema(
140+
properties: {
141+
filepath: { type: "string", description: "Path to the Ruby source file." },
142+
method_name: { type: "string", description: "The name of the method to extract." }
143+
},
144+
required: ["filepath", "method_name"]
145+
)
146+
output_schema(
147+
properties: {
148+
body: { type: "string" }
149+
},
150+
required: ["body"]
151+
)
152+
153+
class << self
154+
def call(server_context:, filepath:, method_name:)
155+
if !File.readable?(filepath)
156+
MCP::Tool::Response.new([MCP::Content::Text.new("Invalid filepath").to_h], error: true)
157+
elsif !(result = Prism.parse_file(filepath)).success?
158+
MCP::Tool::Response.new([MCP::Content::Text.new("Failed to parse file").to_h], error: true)
159+
elsif !(body = find(result.value, method_name))
160+
MCP::Tool::Response.new([MCP::Content::Text.new("Method not found").to_h], error: true)
161+
else
162+
result = { body: body }
163+
output_schema.validate_result(result)
164+
165+
MCP::Tool::Response.new([{ type: "text", text: result.to_json }], structured_content: result)
166+
end
167+
end
168+
169+
private
170+
171+
def find(node, method_name)
172+
name = method_name.to_sym
173+
node.breadth_first_search do |child|
174+
break child.slice if child.is_a?(Prism::DefNode) && child.name == name
175+
end
176+
end
177+
end
178+
end
179+
180+
tools = [
181+
RakeTool,
182+
CFunctionTool,
183+
RubyMethodTool
184+
]
185+
186+
server = MCP::Server.new(name: "Prism", tools: tools)
187+
transport = MCP::Server::Transports::StdioTransport.new(server)
188+
transport.open

0 commit comments

Comments
 (0)