legion-llm 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5b3ae7660acb21a0a098c2154e5046b9349619c33df4725cd9ab0ad183fd5f7
4
- data.tar.gz: 5c02100a171c87a11108d8993367359ee0fd46b1fd9212bb43e34e0f6da2ba62
3
+ metadata.gz: 56b729952a9d16d1b1ab83a669d86d7be4b32b760e3d96e57e19bec1c6158fff
4
+ data.tar.gz: 85336f8cbe3224d03bdc2c93f923711ae081e15fbea41ff4eee0e45a45a99faf
5
5
  SHA512:
6
- metadata.gz: a7c118efb871b25592b6787c5c03cd8af3bdb3cdd034f468dcaba51072d014a0bbd2e7ff38532503729765f73086717080c13dd2f02393413317c59f13a50598
7
- data.tar.gz: ce2220b0a54853125c1271a9c74d38debf389b0b8f27a380d1c3c872bb6c1860d16ff22b900c4ecf388d955331f7fbef05f487e9f19f63673321c85cf0a5b0fb
6
+ metadata.gz: 6bfbe51c0ebb5ece47ebe20e17d925051dcd22ce5a084040f20207b9498597fb2b763aa68933a23aeacbd82d5d064365dd71f3dadaca7ddda4dd282e17d12d90
7
+ data.tar.gz: 7e42e9ee15e04a3ca99d1b00a1355d8c261d3231b006b160e741da581a520787fa3a171afad44b25db28afeec92419fde0aab9508a33a872c9e6a304859ab46e
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.6.6] - 2026-04-01
6
+
7
+ ### Added
8
+ - `McpToolAdapter` — wraps MCP server tool classes as RubyLLM::Tool instances for LLM session injection
9
+ - Pipeline `McpDiscovery` step discovers both server-side (Legion::MCP::Server) and client-side (MCP::Client::Pool) tools
10
+ - Tool name sanitization: dots replaced with underscores for Bedrock compatibility (`[a-zA-Z0-9_-]+`)
11
+
12
+ ### Fixed
13
+ - Skip RubyLLM-based embedding health check for Azure provider since it uses direct HTTP with SNI host injection
14
+
5
15
  ## [0.6.5] - 2026-04-01
6
16
 
7
17
  ### Fixed
@@ -316,18 +316,27 @@ module Legion
316
316
  raise 'Azure OpenAI embedding not configured (llm.providers.azure.api_base required)' unless api_base
317
317
 
318
318
  host = URI.parse(api_base).host
319
- target = ip || host
319
+ target_ip = ip
320
320
  path = "/openai/deployments/#{model}/embeddings?api-version=2024-02-01"
321
+ Legion::Logging.info "Azure embed connecting to #{host}:443 (ip_override=#{target_ip.inspect})" if defined?(Legion::Logging)
321
322
 
322
323
  require 'net/http'
323
- http = Net::HTTP.new(target, 443)
324
+ require 'openssl'
325
+ http = Net::HTTP.new(host, 443)
324
326
  http.use_ssl = true
325
- http.open_timeout = 5
327
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
328
+ http.open_timeout = 10
326
329
  http.read_timeout = 30
327
330
 
331
+ # When an IP override is set, resolve the FQDN to the private endpoint IP
332
+ # while keeping the FQDN as SNI for TLS handshake
333
+ if target_ip
334
+ addr = Addrinfo.tcp(target_ip, 443)
335
+ http.ipaddr = addr.ip_address
336
+ end
337
+
328
338
  req = Net::HTTP::Post.new(path)
329
339
  req['Content-Type'] = 'application/json'
330
- req['Host'] = host
331
340
  req['api-key'] = api_key
332
341
  body = { input: input }
333
342
  body[:dimensions] = dimensions || TARGET_DIMENSION
@@ -76,6 +76,22 @@ module Legion
76
76
 
77
77
  private
78
78
 
79
+ def inject_discovered_tools(session)
80
+ return unless defined?(::Legion::MCP) && ::Legion::MCP.respond_to?(:server)
81
+
82
+ server = ::Legion::MCP.server
83
+ return unless server.respond_to?(:tool_registry)
84
+
85
+ server.tool_registry.each do |mcp_tool_class|
86
+ adapter = McpToolAdapter.new(mcp_tool_class)
87
+ session.with_tool(adapter)
88
+ rescue StandardError => e
89
+ @warnings << "Failed to inject tool: #{e.message}"
90
+ end
91
+ rescue StandardError => e
92
+ @warnings << "Tool injection error: #{e.message}"
93
+ end
94
+
79
95
  def execute_steps
80
96
  executed = 0
81
97
  skipped = 0
@@ -403,7 +419,13 @@ module Legion
403
419
  session.with_tool(tool) if tool.is_a?(Class)
404
420
  end
405
421
 
406
- ToolRegistry.tools.each { |t| session.with_tool(t) } if defined?(ToolRegistry)
422
+ if defined?(ToolRegistry)
423
+ ToolRegistry.tools.each do |t|
424
+ Legion::Logging.fatal("Injecting ToolRegistry tool: #{t.class} #{t.respond_to?(:tool_name) ? t.tool_name : t}") if defined?(Legion::Logging)
425
+ session.with_tool(t)
426
+ end
427
+ end
428
+ inject_discovered_tools(session)
407
429
 
408
430
  injected_system = EnrichmentInjector.inject(
409
431
  system: @request.system,
@@ -545,6 +567,7 @@ module Legion
545
567
 
546
568
  (@request.tools || []).each { |tool| session.with_tool(tool) if tool.is_a?(Class) }
547
569
  ToolRegistry.tools.each { |t| session.with_tool(t) } if defined?(ToolRegistry)
570
+ inject_discovered_tools(session)
548
571
 
549
572
  messages = @request.messages
550
573
  prior = messages.size > 1 ? messages[0..-2] : []
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm'
4
+
5
+ module Legion
6
+ module LLM
7
+ module Pipeline
8
+ class McpToolAdapter < RubyLLM::Tool
9
+ def initialize(mcp_tool_class)
10
+ @mcp_tool_class = mcp_tool_class
11
+ raw_name = mcp_tool_class.respond_to?(:tool_name) ? mcp_tool_class.tool_name : mcp_tool_class.name.to_s
12
+ @tool_name = raw_name.tr('.', '_')
13
+ @tool_desc = mcp_tool_class.respond_to?(:description) ? mcp_tool_class.description.to_s : ''
14
+ @tool_schema = mcp_tool_class.respond_to?(:input_schema) ? mcp_tool_class.input_schema : nil
15
+ super()
16
+ end
17
+
18
+ def name
19
+ @tool_name
20
+ end
21
+
22
+ def description
23
+ @tool_desc
24
+ end
25
+
26
+ def params_schema
27
+ return @params_schema if defined?(@params_schema)
28
+
29
+ @params_schema = (RubyLLM::Utils.deep_stringify_keys(@tool_schema) if @tool_schema.is_a?(Hash))
30
+ end
31
+
32
+ def execute(**)
33
+ result = @mcp_tool_class.call(**)
34
+ if result.is_a?(Hash) && result[:content]
35
+ result[:content].map { |c| c[:text] || c['text'] }.compact.join("\n")
36
+ elsif result.is_a?(String)
37
+ result
38
+ else
39
+ result.to_s
40
+ end
41
+ rescue StandardError => e
42
+ "Tool error: #{e.message}"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -7,35 +7,22 @@ module Legion
7
7
  module McpDiscovery
8
8
  def step_mcp_discovery
9
9
  @discovered_tools ||= []
10
-
11
- unless defined?(::Legion::MCP::Client::Pool)
12
- @warnings << 'MCP Client unavailable for tool discovery'
13
- record_mcp_timeline(0)
14
- return
15
- end
16
-
17
10
  start_time = Time.now
18
- mcp_tools = ::Legion::MCP::Client::Pool.all_tools
19
11
 
20
- mcp_tools.each do |tool|
21
- @discovered_tools << {
22
- name: tool[:name],
23
- description: tool[:description],
24
- parameters: tool[:input_schema],
25
- source: tool[:source]
26
- }
27
- end
12
+ discover_server_tools
13
+ discover_client_tools
28
14
 
29
- if mcp_tools.any?
30
- servers = mcp_tools.map { |t| t.dig(:source, :server) }.uniq
15
+ total = @discovered_tools.size
16
+ if total.positive?
17
+ sources = @discovered_tools.filter_map { |t| t.dig(:source, :server) || t.dig(:source, :type) }.uniq
31
18
  @enrichments['mcp:tool_discovery'] = {
32
- content: "#{mcp_tools.length} tools from #{servers.length} servers",
33
- data: { tool_count: mcp_tools.length, servers: servers },
19
+ content: "#{total} tools from #{sources.length} sources",
20
+ data: { tool_count: total, sources: sources },
34
21
  timestamp: Time.now
35
22
  }
36
23
  end
37
24
 
38
- record_mcp_timeline(mcp_tools.length, start_time)
25
+ record_mcp_timeline(total, start_time)
39
26
  rescue StandardError => e
40
27
  @warnings << "MCP discovery error: #{e.message}"
41
28
  record_mcp_timeline(0)
@@ -43,6 +30,42 @@ module Legion
43
30
 
44
31
  private
45
32
 
33
+ def discover_server_tools
34
+ return unless defined?(::Legion::MCP) && ::Legion::MCP.respond_to?(:server)
35
+
36
+ server = ::Legion::MCP.server
37
+ return unless server.respond_to?(:tool_registry)
38
+
39
+ server.tool_registry.each do |tool_class|
40
+ name = tool_class.respond_to?(:tool_name) ? tool_class.tool_name : tool_class.name
41
+ desc = tool_class.respond_to?(:description) ? tool_class.description : ''
42
+ schema = tool_class.respond_to?(:input_schema) ? tool_class.input_schema : {}
43
+ @discovered_tools << {
44
+ name: name,
45
+ description: desc,
46
+ parameters: schema,
47
+ source: { type: :server, server: 'legion' }
48
+ }
49
+ end
50
+ rescue StandardError => e
51
+ @warnings << "Server tool discovery error: #{e.message}"
52
+ end
53
+
54
+ def discover_client_tools
55
+ return unless defined?(::Legion::MCP::Client::Pool)
56
+
57
+ ::Legion::MCP::Client::Pool.all_tools.each do |tool|
58
+ @discovered_tools << {
59
+ name: tool[:name],
60
+ description: tool[:description],
61
+ parameters: tool[:input_schema],
62
+ source: tool[:source]
63
+ }
64
+ end
65
+ rescue StandardError => e
66
+ @warnings << "Client tool discovery error: #{e.message}"
67
+ end
68
+
46
69
  def record_mcp_timeline(count, start_time = nil)
47
70
  duration = start_time ? ((Time.now - start_time) * 1000).to_i : 0
48
71
  @timeline.record(
@@ -11,6 +11,7 @@ require_relative 'pipeline/gaia_caller'
11
11
  require_relative 'pipeline/tool_dispatcher'
12
12
  require_relative 'pipeline/enrichment_injector'
13
13
  require_relative 'pipeline/steps'
14
+ require_relative 'pipeline/mcp_tool_adapter'
14
15
  require_relative 'pipeline/executor'
15
16
 
16
17
  module Legion
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module LLM
5
- VERSION = '0.6.5'
5
+ VERSION = '0.6.6'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.5
4
+ version: 0.6.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -265,6 +265,7 @@ files:
265
265
  - lib/legion/llm/pipeline/enrichment_injector.rb
266
266
  - lib/legion/llm/pipeline/executor.rb
267
267
  - lib/legion/llm/pipeline/gaia_caller.rb
268
+ - lib/legion/llm/pipeline/mcp_tool_adapter.rb
268
269
  - lib/legion/llm/pipeline/profile.rb
269
270
  - lib/legion/llm/pipeline/request.rb
270
271
  - lib/legion/llm/pipeline/response.rb