Class: Rubino::MCP::MCPToolWrapper

Inherits:
Tools::Base show all
Defined in:
lib/rubino/mcp/mcp_tool_wrapper.rb

Overview

Wraps an MCP tool (from ruby_llm-mcp) into the Rubino::Tools::Base interface. This allows MCP tools to be used seamlessly alongside built-in tools.

Constant Summary collapse

MAX_NAME_LENGTH =

Cap on the (prefixed) tool name length. A misbehaving MCP server can advertise an absurdly long tool name (e.g. 20k chars), which would be registered uncapped, blow up the ‘tools` table, and 400 at the provider.

64

Instance Attribute Summary collapse

Attributes inherited from Tools::Base

#cancel_token, #read_tracker, #stream_chunk, #stream_kind

Instance Method Summary collapse

Methods inherited from Tools::Base

#cancellation_requested?, #config_key, #emit_chunk, #risky?, workspace_root, workspace_roots

Constructor Details

#initialize(mcp_tool, server_name:) ⇒ MCPToolWrapper

Returns a new instance of MCPToolWrapper.



15
16
17
18
# File 'lib/rubino/mcp/mcp_tool_wrapper.rb', line 15

def initialize(mcp_tool, server_name:)
  @mcp_tool = mcp_tool
  @server_name = server_name
end

Instance Attribute Details

#mcp_toolObject (readonly)

Returns the value of attribute mcp_tool.



8
9
10
# File 'lib/rubino/mcp/mcp_tool_wrapper.rb', line 8

def mcp_tool
  @mcp_tool
end

#server_nameObject (readonly)

Returns the value of attribute server_name.



8
9
10
# File 'lib/rubino/mcp/mcp_tool_wrapper.rb', line 8

def server_name
  @server_name
end

Instance Method Details

#call(arguments) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/rubino/mcp/mcp_tool_wrapper.rb', line 50

def call(arguments)
  result = @mcp_tool.execute(**symbolize_keys(arguments))
  # ruby_llm-mcp reports tool failures by RETURNING `{ error: "…" }`
  # instead of raising. Map both failure paths onto the registry's
  # "Error: …" convention (Tools::Result#errorish?) so an errored MCP
  # call renders ✗ like any built-in tool, not "✓ done" (#172).
  error = result[:error] || result["error"] if result.is_a?(Hash)
  return "Error: MCP tool #{@server_name}/#{@mcp_tool.name}: #{error}" if error

  result.to_s
rescue StandardError => e
  "Error: MCP tool #{@server_name}/#{@mcp_tool.name}: #{e.message}"
end

#descriptionObject



27
28
29
# File 'lib/rubino/mcp/mcp_tool_wrapper.rb', line 27

def description
  @mcp_tool.description
end

#input_schemaObject



31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/rubino/mcp/mcp_tool_wrapper.rb', line 31

def input_schema
  # The server-advertised JSON schema lives in RubyLLM::MCP::Tool#params_schema.
  # The inherited RubyLLM::Tool#parameters DSL accessor is ALWAYS empty for
  # MCP tools — forwarding it sent every tool to the model with `parameters:
  # {}`, so the model had to guess argument names and every call failed
  # server-side validation with -32602 (#170).
  schema = @mcp_tool.params_schema if @mcp_tool.respond_to?(:params_schema)
  # Coerce anything that isn't a Hash (nil, or a truthy non-Hash like a
  # string) to a valid empty object schema. A server advertising a
  # non-Hash `inputSchema` would otherwise poison the whole wire tool
  # list and 400 every subsequent model call (S1-MCP-1).
  schema.is_a?(Hash) ? schema : { type: "object", properties: {} }
end

#nameObject



20
21
22
23
24
25
# File 'lib/rubino/mcp/mcp_tool_wrapper.rb', line 20

def name
  # Prefix with server name to avoid collisions. Cap the length so a
  # hostile/buggy server can't register a giant name that breaks the
  # `tools` table or 400s the provider (S1-MCP-2).
  "#{@server_name}_#{@mcp_tool.name}"[0, MAX_NAME_LENGTH]
end

#risk_levelObject



45
46
47
48
# File 'lib/rubino/mcp/mcp_tool_wrapper.rb', line 45

def risk_level
  # MCP tools are external, default to medium risk
  :medium
end

#to_tool_definitionObject

Override to provide the raw MCP tool definition for LLM



65
66
67
68
69
70
71
# File 'lib/rubino/mcp/mcp_tool_wrapper.rb', line 65

def to_tool_definition
  {
    name: name,
    description: description,
    parameters: input_schema
  }
end