Skip to content

Commit 34fa53c

Browse files
authored
Merge pull request #10 from chrisbutcher/fix-sorbet-runtime-compatibility
Support Sorbet typed tools
2 parents 89ef9d8 + 6e78209 commit 34fa53c

File tree

5 files changed

+87
-3
lines changed

5 files changed

+87
-3
lines changed

lib/model_context_protocol/server.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ def call_tool(request)
213213
end
214214

215215
begin
216-
call_params = tool.method(:call).parameters.flatten
216+
call_params = tool_call_parameters(tool)
217+
217218
if call_params.include?(:server_context)
218219
tool.call(**arguments.transform_keys(&:to_sym), server_context:).to_h
219220
else
@@ -274,5 +275,24 @@ def index_resources_by_uri(resources)
274275
hash[resource.uri] = resource
275276
end
276277
end
278+
279+
def tool_call_parameters(tool)
280+
method_def = tool_call_method_def(tool)
281+
method_def.parameters.flatten
282+
end
283+
284+
def tool_call_method_def(tool)
285+
method = tool.method(:call)
286+
287+
if defined?(T::Utils) && T::Utils.respond_to?(:signature_for_method)
288+
sorbet_typed_method_definition = T::Utils.signature_for_method(method)&.method
289+
290+
# Return the Sorbet typed method definition if it exists, otherwise fallback to original method
291+
# definition if Sorbet is defined but not used by this tool.
292+
sorbet_typed_method_definition || method
293+
else
294+
method
295+
end
296+
end
277297
end
278298
end

model_context_protocol.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
2929

3030
spec.add_dependency("json_rpc_handler", "~> 0.1")
3131
spec.add_development_dependency("activesupport")
32+
spec.add_development_dependency("sorbet-static-and-runtime")
3233
end

test/model_context_protocol/server_test.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# typed: true
12
# frozen_string_literal: true
23

34
require "test_helper"
@@ -256,6 +257,43 @@ class ServerTest < ActiveSupport::TestCase
256257
assert_instrumentation_data({ method: "tools/call", tool_name: })
257258
end
258259

260+
test "#handle_json tools/call executes tool and returns result, when the tool is typed with Sorbet" do
261+
class TypedTestTool < Tool
262+
tool_name "test_tool"
263+
description "a test tool for testing"
264+
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
265+
266+
class << self
267+
extend T::Sig
268+
269+
sig { params(message: String, server_context: T.nilable(T.untyped)).returns(Tool::Response) }
270+
def call(message:, server_context: nil)
271+
Tool::Response.new([{ type: "text", content: "OK" }])
272+
end
273+
end
274+
end
275+
276+
request = JSON.generate({
277+
jsonrpc: "2.0",
278+
method: "tools/call",
279+
params: { name: "test_tool", arguments: { message: "Hello, world!" } },
280+
id: 1,
281+
})
282+
283+
server = Server.new(
284+
name: @server_name,
285+
tools: [TypedTestTool],
286+
prompts: [@prompt],
287+
resources: [@resource],
288+
resource_templates: [@resource_template],
289+
)
290+
291+
raw_response = server.handle_json(request)
292+
response = JSON.parse(raw_response, symbolize_names: true) if raw_response
293+
294+
assert_equal({ content: [{ type: "text", content: "OK" }], isError: false }, response[:result])
295+
end
296+
259297
test "#handle tools/call returns internal error and reports exception if the tool raises an error" do
260298
@server.configuration.exception_reporter.expects(:call).with do |exception, server_context|
261299
assert_not_nil exception

test/model_context_protocol/tool_test.rb

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# typed: true
12
# frozen_string_literal: true
23

34
require "test_helper"
@@ -17,7 +18,7 @@ class TestTool < Tool
1718
)
1819

1920
class << self
20-
def call(message, server_context: nil)
21+
def call(message:, server_context: nil)
2122
Tool::Response.new([{ type: "text", content: "OK" }])
2223
end
2324
end
@@ -42,7 +43,7 @@ def call(message, server_context: nil)
4243

4344
test "#call invokes the tool block and returns the response" do
4445
tool = TestTool
45-
response = tool.call("test")
46+
response = tool.call(message: "test")
4647
assert_equal response.content, [{ type: "text", content: "OK" }]
4748
assert_equal response.is_error, false
4849
end
@@ -203,5 +204,27 @@ class UpdatableAnnotationsTool < Tool
203204
tool.annotations(title: "Updated")
204205
assert_equal tool.annotations_value.title, "Updated"
205206
end
207+
208+
test "#call with Sorbet typed tools invokes the tool block and returns the response" do
209+
class TypedTestTool < Tool
210+
tool_name "test_tool"
211+
description "a test tool for testing"
212+
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
213+
214+
class << self
215+
extend T::Sig
216+
217+
sig { params(message: String, server_context: T.nilable(T.untyped)).returns(Tool::Response) }
218+
def call(message:, server_context: nil)
219+
Tool::Response.new([{ type: "text", content: "OK" }])
220+
end
221+
end
222+
end
223+
224+
tool = TypedTestTool
225+
response = tool.call(message: "test")
226+
assert_equal response.content, [{ type: "text", content: "OK" }]
227+
assert_equal response.is_error, false
228+
end
206229
end
207230
end

test/test_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
require "active_support"
1414
require "active_support/test_case"
1515

16+
require "sorbet-runtime"
17+
1618
require_relative "instrumentation_test_helper"
1719

1820
Minitest::Reporters.use!(Minitest::Reporters::ProgressReporter.new)

0 commit comments

Comments
 (0)