Class: Collavre::McpService

Inherits:
Object
  • Object
show all
Defined in:
app/services/collavre/mcp_service.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.available_tools(user) ⇒ Object

Fetch and filter available tools for the given user. Returns an array of tool hashes with :name, :description, :params keys.



116
117
118
119
120
121
122
123
124
125
126
# File 'app/services/collavre/mcp_service.rb', line 116

def self.available_tools(user)
  return [] unless defined?(RailsMcpEngine)

  RailsMcpEngine::Engine.build_tools!
  result = ::Tools::MetaToolService.new.call(action: "list", tool_name: nil, query: nil, arguments: nil)
  tool_list = Array(result[:tools])
  filter_tools(tool_list, user)
rescue StandardError => e
  Rails.logger.error("Failed to load available tools: #{e.message}")
  []
end

.delete_tool(tool_name) ⇒ Object



128
129
130
131
132
133
134
135
136
# File 'app/services/collavre/mcp_service.rb', line 128

def self.delete_tool(tool_name)
  result = ::Tools::MetaToolWriteService.new.delete_tool(tool_name)

  if result[:error]
    Rails.logger.error("Failed to delete tool #{tool_name}: #{result[:error]}")
  end
rescue StandardError => e
  Rails.logger.error("Failed to delete tool #{tool_name}: #{e.message}")
end

.filter_tools(tools, user) ⇒ Object



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
98
99
100
101
102
103
104
105
106
# File 'app/services/collavre/mcp_service.rb', line 63

def self.filter_tools(tools, user)
  return [] if tools.blank?

  # Identify dynamic tools (user-defined) vs system tools.
  # Tools can be objects (FastMcp::Tool) or Hashes (from MetaToolService)
  registered_names = tools.map do |tool|
    if tool.respond_to?(:tool_name)
      tool.tool_name
    elsif tool.is_a?(Hash)
      tool[:name] || tool["name"]
    end
  end

  # Check strict loading? No, simple where is fine.
  dynamic_tools = McpTool.where(name: registered_names).includes(:creative)
  dynamic_tool_names = dynamic_tools.pluck(:name).to_set

  # Build set of tool names the user has permission to run
  # User needs write permission on the creative to run its tools
  accessible_tool_names = if user
                            dynamic_tools.select do |mcp_tool|
                              mcp_tool.creative&.has_permission?(user, :write)
                            end.map(&:name).to_set
  else
                            Set.new
  end

  tools.select do |tool|
    name = if tool.respond_to?(:tool_name)
             tool.tool_name
    elsif tool.is_a?(Hash)
             tool[:name] || tool["name"]
    else
             nil
    end
    if dynamic_tool_names.include?(name)
      # It is a dynamic tool; user must have write permission on its creative.
      accessible_tool_names.include?(name)
    else
      # It is a system tool (not in McpTool database); allow it.
      true
    end
  end
end

.load_active_toolsObject



108
109
110
111
112
# File 'app/services/collavre/mcp_service.rb', line 108

def self.load_active_tools
  McpTool.active.find_each do |tool|
    register_tool_from_source(tool.source_code)
  end
end

.register_tool_from_source(source_code) ⇒ Object

— Registration Logic (from MetaToolService) —



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
# File 'app/services/collavre/mcp_service.rb', line 8

def self.register_tool_from_source(source_code)
  # Extract tool name for logging context
  tool_name_match = source_code.match(/tool_name\s+["'](.+?)["']/)
  tool_name = tool_name_match ? tool_name_match[1] : "unknown_tool"

  before_call = proc do |tool_instance, method_name, args|
    # Store args for after_call access if needed, or just log start
    # Using thread local to pass data to after_call if we want to correlate exact timing or args
    Thread.current[:mcp_tool_args_stack] ||= []
    Thread.current[:mcp_tool_args_stack].push(args)
  end

  after_call = proc do |tool_instance, method_name, result|
    # Retrieve args
    args = Thread.current[:mcp_tool_args_stack]&.pop || {}

    # Create activity log
    # We need a user to attribute this to.
    # If executed in a background job (Task), Current.user is set.
    # If executed via API, Current.user is set.
    user = Current.user
    creative = tool_instance&.try(:creative_context) rescue nil # Assuming some way to get context if needed, or nil

    ActivityLog.create!(
      activity: "tool_execution",
      user: user,
      creative: creative, # Optional: if we can link it back to a creative
      log: {
        tool_name: tool_name,
        method: method_name,
        args: args,
        result: result
      }
    )
  rescue StandardError => e
    Rails.logger.error("Failed to log tool activity: #{e.message}")
  end

  result = ::Tools::MetaToolWriteService.new.register_tool_from_source(
    source: source_code,
    before_call: before_call,
    after_call: after_call
  )
  Rails.logger.info("Registered tool: #{result}")

  if result[:error]
    error_msg = "Failed to register tool: #{result[:error]}"
    Rails.logger.error(error_msg)
    raise error_msg
  end
rescue StandardError => e
  Rails.logger.error("Failed to register tool from source: #{e.message}")
  raise e
end

Instance Method Details

#update_from_creative(input_creative) ⇒ Object

— Creative Parsing Logic (from MetaToolWriteService) —



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'app/services/collavre/mcp_service.rb', line 140

def update_from_creative(input_creative)
  creative = input_creative.effective_origin
  return unless creative.description.present?

  # Parse HTML to find code blocks
  doc = Nokogiri::HTML.fragment(creative.description)

  # Track found tools to identify removals
  found_tool_names = []

  # Find all code blocks.
  # Lexical uses <pre class="lexical-code-block">.
  # Standard markdown often uses <code>.
  doc.css("pre.lexical-code-block, code").each do |node|
    # Create a copy to manipulate
    working_node = node.dup

    # Replace <br> tags with newlines
    working_node.search("br").each { |br| br.replace("\n") }

    code = working_node.text

    # Check if it looks like a tool definition
    if code.include?("extend ToolMeta")
      tool_name = process_tool_definition(creative, code)
      found_tool_names << tool_name if tool_name
    end
  end

  # Remove tools that are no longer in the description
  # Ensure we look at the effective origin's tools
  creative.mcp_tools.where.not(name: found_tool_names).destroy_all
end