-
Notifications
You must be signed in to change notification settings - Fork 91
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
Describe the bug
With Sorbet strict typing on a tool definition (example tool below), internal MCP server introspection of tool call method parameters breaks, since it is re-written by sorbet-runtime's call validation.
i.e. Despite a tool defining call with the param server_context:, the logic falls incorrectly into the else case of this logic.
Which leads to a runtime error of:
> tool.call(**arguments.transform_keys(&:to_sym)).to_h
=> eval error: missing keyword: :server_context
To Reproduce
- Setup an MCP server with a Sorbet-typed tool as below
- Try invoking the tool with Claude IDE (for example)
- 💥
# typed: strict
# frozen_string_literal: true
class CounterTool < ModelContextProtocol::Tool
extend T::Sig
description "A simple counter tool that can increment and read a counter value"
input_schema(
properties: {
action: {
type: "string",
enum: ["increment", "read"],
description: "The action to perform: increment the counter or read its current value",
},
},
required: ["action"],
)
# Track the counter state
@@counter = T.let(0, Integer)
sig { params(action: String, server_context: T::Hash[T.any(String, Symbol), T.untyped]).returns(ModelContextProtocol::Tool::Response) }
def self.call(action:, server_context:)
case action
when "increment"
@@counter += 1
ModelContextProtocol::Tool::Response.new(
[{ type: "text", text: "Counter incremented to #{@@counter}" }],
)
when "read"
ModelContextProtocol::Tool::Response.new(
[{ type: "text", text: "Current counter value is #{@@counter}" }],
)
else
ModelContextProtocol::Tool::Response.new(
[{ type: "text", text: "Invalid action. Use 'increment' or 'read'." }],
is_error: true,
)
end
end
endExpected behavior
Sorbet typed MCP tools should "just work".
Suggested solution
See #10
Sorbet provides T::Utils.signature_for_method, which we can use if it's defined to introspect the original method's parameters.
# Calling code
def call_tool(request)
# ...
call_params = method_parameters(tool.method(:call))
if call_params.include?(:server_context)
tool.call(**arguments.transform_keys(&:to_sym), server_context:).to_h
else
tool.call(**arguments.transform_keys(&:to_sym)).to_h
end
# ...
private
def method_parameters(method)
default_value = method.parameters.flatten
if defined?(T::Utils) && T::Utils.respond_to?(:signature_for_method)
method_sig = T::Utils.signature_for_method(method)
if method_sig
method_sig.parameters.flatten
else
default_value
end
else
default_value
end
endMetadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working