Skip to content

Commit 84e8384

Browse files
committed
Support Sorbet typed tools
1 parent 87cd467 commit 84e8384

File tree

5 files changed

+92
-1
lines changed

5 files changed

+92
-1
lines changed

lib/model_context_protocol/server.rb

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

199199
begin
200-
call_params = tool.method(:call).parameters.flatten
200+
call_params = tool_call_parameters(tool)
201+
201202
if call_params.include?(:server_context)
202203
tool.call(**arguments.transform_keys(&:to_sym), server_context:).to_h
203204
else
@@ -258,5 +259,24 @@ def index_resources_by_uri(resources)
258259
hash[resource.uri] = resource
259260
end
260261
end
262+
263+
def tool_call_parameters(tool)
264+
method_def = tool_call_method_def(tool)
265+
method_def.parameters.flatten
266+
end
267+
268+
def tool_call_method_def(tool)
269+
method = tool.method(:call)
270+
271+
if defined?(T::Utils) && T::Utils.respond_to?(:signature_for_method)
272+
sorbet_typed_method_definition = T::Utils.signature_for_method(method)&.method
273+
274+
# Return the Sorbet typed method definition if it exists, otherwise fallback to original method
275+
# definition if Sorbet is defined but not used by this tool.
276+
sorbet_typed_method_definition || method
277+
else
278+
method
279+
end
280+
end
261281
end
262282
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: 30 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"
@@ -203,5 +204,34 @@ 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+
annotations(
214+
title: "Test Tool",
215+
read_only_hint: true,
216+
destructive_hint: false,
217+
idempotent_hint: true,
218+
open_world_hint: false,
219+
)
220+
221+
class << self
222+
extend T::Sig
223+
224+
sig { params(message: String, server_context: T.nilable(T.untyped)).returns(Tool::Response) }
225+
def call(message, server_context: nil)
226+
Tool::Response.new([{ type: "text", content: "OK" }])
227+
end
228+
end
229+
end
230+
231+
tool = TypedTestTool
232+
response = tool.call("test")
233+
assert_equal response.content, [{ type: "text", content: "OK" }]
234+
assert_equal response.is_error, false
235+
end
206236
end
207237
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)