Skip to content

Sorbet runtime call validation breaks MCP tool invocation #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
chrisbutcher opened this issue May 16, 2025 · 0 comments · May be fixed by #10
Open

Sorbet runtime call validation breaks MCP tool invocation #9

chrisbutcher opened this issue May 16, 2025 · 0 comments · May be fixed by #10
Labels
bug Something isn't working

Comments

@chrisbutcher
Copy link

chrisbutcher commented May 16, 2025

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.

Image

Which leads to a runtime error of:

> tool.call(**arguments.transform_keys(&:to_sym)).to_h
=> eval error: missing keyword: :server_context

To Reproduce

  1. Setup an MCP server with a Sorbet-typed tool as below
  2. Try invoking the tool with Claude IDE (for example)
  3. 💥
# 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
end

Expected 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
end
@chrisbutcher chrisbutcher added the bug Something isn't working label May 16, 2025
@chrisbutcher chrisbutcher linked a pull request May 16, 2025 that will close this issue
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant