Class: Legate::Mcp::ToolWrapper

Inherits:
Tool
  • Object
show all
Defined in:
lib/legate/mcp/tool_wrapper.rb

Overview

Base class for dynamically created Legate::Tool wrappers around external MCP tools. Instances of anonymous subclasses generated by ‘from_mcp_schema` are used.

Class Attribute Summary collapse

Attributes inherited from Tool

#description, #name, #parameters

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Tool

define_metadata, #execute, inherited, #initialize, #validate_and_coerce_params, #validate_params

Methods included from Tool::MetadataDsl

included

Constructor Details

This class inherits a constructor from Legate::Tool

Class Attribute Details

.mcp_clientObject

References stored on the dynamically created subclass



20
21
22
# File 'lib/legate/mcp/tool_wrapper.rb', line 20

def mcp_client
  @mcp_client
end

.mcp_input_schemaObject

References stored on the dynamically created subclass



20
21
22
# File 'lib/legate/mcp/tool_wrapper.rb', line 20

def mcp_input_schema
  @mcp_input_schema
end

.mcp_tool_nameObject

References stored on the dynamically created subclass



20
21
22
# File 'lib/legate/mcp/tool_wrapper.rb', line 20

def mcp_tool_name
  @mcp_tool_name
end

Class Method Details

.from_mcp_schema(mcp_schema, mcp_client, tool_registry) ⇒ Class

Creates a new anonymous Legate::Tool subclass that wraps an external MCP tool. Registers the new tool class with the provided Legate::ToolRegistry instance.

Parameters:

  • mcp_schema (Hash)

    The tool schema hash from MCP server.

  • mcp_client (Legate::Mcp::Client)

    The client instance.

  • tool_registry (Legate::ToolRegistry)

    The specific registry instance to register with.

Returns:

  • (Class)

    The newly created anonymous ToolWrapper subclass, or nil.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/legate/mcp/tool_wrapper.rb', line 30

def self.from_mcp_schema(mcp_schema, mcp_client, tool_registry)
  # Validate inputs including the registry
  unless mcp_schema.is_a?(Hash) && mcp_schema[:name] &&
         mcp_client.is_a?(Legate::Mcp::Client) &&
         tool_registry.is_a?(Legate::ToolRegistry)
    Mcp.logger.error('Invalid input for ToolWrapper.from_mcp_schema: Schema, Client, or Registry invalid.')
    return nil
  end

  mcp_name = mcp_schema[:name]
  mcp_description = mcp_schema[:description] || "MCP Tool: #{mcp_name}"
  mcp_input_schema = mcp_schema[:inputSchema] || {}
  mcp_properties = mcp_input_schema[:properties] || {}
  mcp_required = mcp_input_schema[:required] || []

  legate_params = Util::SchemaConverter.json_to_legate(mcp_properties, mcp_required)

  # Create anonymous class
  wrapper_class = Class.new(ToolWrapper) do
    # --- CAPTURE local variables for use in method definitions ---
    captured_mcp_name_sym = mcp_name.to_sym
    captured_mcp_description = mcp_description
    captured_legate_params = legate_params

    # Store references needed for execution on the class itself
    self.mcp_client = mcp_client
    self.mcp_tool_name = mcp_name # Keep original string name for execution
    self.mcp_input_schema = mcp_input_schema

    # --- Define the tool_metadata method explicitly on this anonymous class ---
    define_singleton_method(:tool_metadata) do
      {
        name: captured_mcp_name_sym,
        description: captured_mcp_description,
        parameters: captured_legate_params
      }
    end

    # --- ALSO explicitly define the individual readers for robustness ---
    # (These might be called by other parts of Legate or ToolRegistry indirectly)
    define_singleton_method(:tool_name) { captured_mcp_name_sym }
    define_singleton_method(:description) { captured_mcp_description }
    define_singleton_method(:parameters_definition) { captured_legate_params }

    # --- Keep the original define_metadata call as well for global registration ---
    # Although redundant for metadata retrieval via tool_metadata, it handles global registration.
    (
      name: captured_mcp_name_sym,
      description: captured_mcp_description,
      parameters: captured_legate_params
    )
  end

  Mcp.logger.info("Created Legate Tool wrapper for MCP tool: '#{mcp_name}'")

  # Register with the PROVIDED registry instance
  begin
    tool_registry.register(mcp_name.to_sym, wrapper_class)
    Mcp.logger.debug("Registered wrapper for MCP tool '#{mcp_name}' with provided ToolRegistry.")
  rescue Legate::ToolRegistry::ToolExistsError => e
    Mcp.logger.warn("MCP Tool '#{mcp_name}' conflicts with an existing tool in provided registry: #{e.message}")
  rescue StandardError => e
    Mcp.logger.error("Failed to register wrapper for MCP tool '#{mcp_name}' with provided registry: #{e.message}")
    return nil
  end

  wrapper_class
end

Instance Method Details

#perform_execution(params, _context) ⇒ Hash

Executes the wrapped MCP tool via the stored MCP client.

Parameters:

  • params (Hash)

    Parameters provided by the Legate agent/planner.

  • context (Legate::ToolContext)

    The execution context (less relevant for external tools).

Returns:

  • (Hash)

    Legate status hash (:success/:error, result:/error_message:).



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/legate/mcp/tool_wrapper.rb', line 104

def perform_execution(params, _context)
  self.class.mcp_client || begin
    Mcp.logger.error("MCP Client not configured for tool wrapper: #{self.class.mcp_tool_name}")
    return { status: :error, error_message: 'Internal configuration error: MCP Client missing.' }
  end

  mcp_tool_name_str = self.class.mcp_tool_name
  mcp_client_instance = self.class.mcp_client

  Mcp.logger.info("Executing wrapped MCP tool '#{mcp_tool_name_str}' with params: #{params.inspect}")

  # TODO: V1.1 - Translate Legate params back to JSON structure based on mcp_input_schema?
  # For V1, assume flat hash structure matches between Legate and MCP tool for basic types.
  mcp_args = params.transform_keys(&:to_s) # Convert symbol keys back to strings for JSON

  begin
    result = mcp_client_instance.call_tool(mcp_tool_name_str, mcp_args)
    Mcp.logger.info("MCP tool '#{mcp_tool_name_str}' executed successfully.")
    { status: :success, result: result }
  rescue Legate::Mcp::RemoteToolError => e
    Mcp.logger.error("MCP tool '#{mcp_tool_name_str}' returned an error: #{e}")
    { status: :error, error_message: "MCP Tool Error: #{e.message}",
      error_details: { code: e.code, data: e.data } }
  rescue Legate::Mcp::ConnectionError, Legate::Mcp::ProtocolError => e
    Mcp.logger.error("MCP communication error during '#{mcp_tool_name_str}' execution: #{e.message}")
    { status: :error, error_message: "MCP Communication Error: #{e.message}" }
  rescue StandardError => e
    Mcp.logger.error("Unexpected error during MCP tool '#{mcp_tool_name_str}' execution: #{e.class} - #{e.message}")
    Mcp.logger.error(e.backtrace.join("\n"))
    { status: :error, error_message: "Internal Error: #{e.message}" }
  end
end