Merge pull request #10 from chrisbutcher/fix-sorbet-runtime-compatibility
Support Sorbet typed tools
This commit is contained in:
commit
34fa53c305
5 changed files with 87 additions and 3 deletions
|
|
@ -213,7 +213,8 @@ module ModelContextProtocol
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
call_params = tool.method(:call).parameters.flatten
|
call_params = tool_call_parameters(tool)
|
||||||
|
|
||||||
if call_params.include?(:server_context)
|
if call_params.include?(:server_context)
|
||||||
tool.call(**arguments.transform_keys(&:to_sym), server_context:).to_h
|
tool.call(**arguments.transform_keys(&:to_sym), server_context:).to_h
|
||||||
else
|
else
|
||||||
|
|
@ -274,5 +275,24 @@ module ModelContextProtocol
|
||||||
hash[resource.uri] = resource
|
hash[resource.uri] = resource
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tool_call_parameters(tool)
|
||||||
|
method_def = tool_call_method_def(tool)
|
||||||
|
method_def.parameters.flatten
|
||||||
|
end
|
||||||
|
|
||||||
|
def tool_call_method_def(tool)
|
||||||
|
method = tool.method(:call)
|
||||||
|
|
||||||
|
if defined?(T::Utils) && T::Utils.respond_to?(:signature_for_method)
|
||||||
|
sorbet_typed_method_definition = T::Utils.signature_for_method(method)&.method
|
||||||
|
|
||||||
|
# Return the Sorbet typed method definition if it exists, otherwise fallback to original method
|
||||||
|
# definition if Sorbet is defined but not used by this tool.
|
||||||
|
sorbet_typed_method_definition || method
|
||||||
|
else
|
||||||
|
method
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,5 @@ Gem::Specification.new do |spec|
|
||||||
|
|
||||||
spec.add_dependency("json_rpc_handler", "~> 0.1")
|
spec.add_dependency("json_rpc_handler", "~> 0.1")
|
||||||
spec.add_development_dependency("activesupport")
|
spec.add_development_dependency("activesupport")
|
||||||
|
spec.add_development_dependency("sorbet-static-and-runtime")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# typed: true
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
@ -256,6 +257,43 @@ module ModelContextProtocol
|
||||||
assert_instrumentation_data({ method: "tools/call", tool_name: })
|
assert_instrumentation_data({ method: "tools/call", tool_name: })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "#handle_json tools/call executes tool and returns result, when the tool is typed with Sorbet" do
|
||||||
|
class TypedTestTool < Tool
|
||||||
|
tool_name "test_tool"
|
||||||
|
description "a test tool for testing"
|
||||||
|
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
|
||||||
|
|
||||||
|
class << self
|
||||||
|
extend T::Sig
|
||||||
|
|
||||||
|
sig { params(message: String, server_context: T.nilable(T.untyped)).returns(Tool::Response) }
|
||||||
|
def call(message:, server_context: nil)
|
||||||
|
Tool::Response.new([{ type: "text", content: "OK" }])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
request = JSON.generate({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
method: "tools/call",
|
||||||
|
params: { name: "test_tool", arguments: { message: "Hello, world!" } },
|
||||||
|
id: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
server = Server.new(
|
||||||
|
name: @server_name,
|
||||||
|
tools: [TypedTestTool],
|
||||||
|
prompts: [@prompt],
|
||||||
|
resources: [@resource],
|
||||||
|
resource_templates: [@resource_template],
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_response = server.handle_json(request)
|
||||||
|
response = JSON.parse(raw_response, symbolize_names: true) if raw_response
|
||||||
|
|
||||||
|
assert_equal({ content: [{ type: "text", content: "OK" }], isError: false }, response[:result])
|
||||||
|
end
|
||||||
|
|
||||||
test "#handle tools/call returns internal error and reports exception if the tool raises an error" do
|
test "#handle tools/call returns internal error and reports exception if the tool raises an error" do
|
||||||
@server.configuration.exception_reporter.expects(:call).with do |exception, server_context|
|
@server.configuration.exception_reporter.expects(:call).with do |exception, server_context|
|
||||||
assert_not_nil exception
|
assert_not_nil exception
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# typed: true
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
@ -17,7 +18,7 @@ module ModelContextProtocol
|
||||||
)
|
)
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def call(message, server_context: nil)
|
def call(message:, server_context: nil)
|
||||||
Tool::Response.new([{ type: "text", content: "OK" }])
|
Tool::Response.new([{ type: "text", content: "OK" }])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -42,7 +43,7 @@ module ModelContextProtocol
|
||||||
|
|
||||||
test "#call invokes the tool block and returns the response" do
|
test "#call invokes the tool block and returns the response" do
|
||||||
tool = TestTool
|
tool = TestTool
|
||||||
response = tool.call("test")
|
response = tool.call(message: "test")
|
||||||
assert_equal response.content, [{ type: "text", content: "OK" }]
|
assert_equal response.content, [{ type: "text", content: "OK" }]
|
||||||
assert_equal response.is_error, false
|
assert_equal response.is_error, false
|
||||||
end
|
end
|
||||||
|
|
@ -203,5 +204,27 @@ module ModelContextProtocol
|
||||||
tool.annotations(title: "Updated")
|
tool.annotations(title: "Updated")
|
||||||
assert_equal tool.annotations_value.title, "Updated"
|
assert_equal tool.annotations_value.title, "Updated"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "#call with Sorbet typed tools invokes the tool block and returns the response" do
|
||||||
|
class TypedTestTool < Tool
|
||||||
|
tool_name "test_tool"
|
||||||
|
description "a test tool for testing"
|
||||||
|
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
|
||||||
|
|
||||||
|
class << self
|
||||||
|
extend T::Sig
|
||||||
|
|
||||||
|
sig { params(message: String, server_context: T.nilable(T.untyped)).returns(Tool::Response) }
|
||||||
|
def call(message:, server_context: nil)
|
||||||
|
Tool::Response.new([{ type: "text", content: "OK" }])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tool = TypedTestTool
|
||||||
|
response = tool.call(message: "test")
|
||||||
|
assert_equal response.content, [{ type: "text", content: "OK" }]
|
||||||
|
assert_equal response.is_error, false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ require "mocha/minitest"
|
||||||
require "active_support"
|
require "active_support"
|
||||||
require "active_support/test_case"
|
require "active_support/test_case"
|
||||||
|
|
||||||
|
require "sorbet-runtime"
|
||||||
|
|
||||||
require_relative "instrumentation_test_helper"
|
require_relative "instrumentation_test_helper"
|
||||||
|
|
||||||
Minitest::Reporters.use!(Minitest::Reporters::ProgressReporter.new)
|
Minitest::Reporters.use!(Minitest::Reporters::ProgressReporter.new)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue