legion-mcp 0.4.5 → 0.5.0

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: de51bbc257bd7eb0c3b573dc4ba04b908656b360c03e92b76d9c1a2aa6fdceeb
4
- data.tar.gz: 68d9bdb57318c9d5fb3dc19d720be5b7f490f4c589bda8bfd3c77fd08850b964
3
+ metadata.gz: 2eff77018e5f5162f17ce2582bee76bff4e78d73e3129c9cf3adac4f40922bd1
4
+ data.tar.gz: fbc6a1ad1463750b1381a8d48e56b36c78a2166fd7514db74e0e1cba7a904df0
5
5
  SHA512:
6
- metadata.gz: 9f76d6e46d90eef89f1c4e2536126501aba765bd8f9bad9436a2a90c77900a3116199c49b7c7b68cba701b74a25c81d7fb58b4fd2fb179a43a55bd4d8b767e4b
7
- data.tar.gz: 1072848b1f4d835d0a9c15dc992435bd6a38aa6cac6d004d6760983c2e66565530026019b69ca613f2dac39eef6316f364421cd809aad2bbf3524f17294ef756
6
+ metadata.gz: 3e098a68af07e9af9c554fa77b116c2de2ed36a076ee332ff5bbb040d96957ec7f5afc390a4b4d743244b3b524a045b9390a19c4ed8ca2cddb279b17006e4a55
7
+ data.tar.gz: 051acdc2bc2c3164400932f98fdc5a79a52f283766c8832d672cfdc44bdeed79c12a38745b450dbb501afe3ced38361418d0bbd279fc6d63ff28b25600ba1d11
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # legion-mcp Changelog
2
2
 
3
+ ## [0.5.0] - 2026-03-23
4
+
5
+ ### Added
6
+ - MCP client: `ServerRegistry` for static (settings) and dynamic (runtime) server registration with health tracking and cooldown-based recovery
7
+ - MCP client: `Connection` class for stdio and HTTP transport connections with TTL-cached tool lists
8
+ - MCP client: `Pool` for long-lived connection management, aggregates tools across all healthy servers
9
+ - MCP client: `Client.boot` loads server registry from `Legion::Settings[:mcp][:servers]` at startup
10
+ - MCP client: `Client.register` / `Client.deregister` for runtime server management
11
+ - `Settings` module with defaults for `servers`, `overrides`, `tool_cache_ttl`, `connect_timeout`, `call_timeout`
12
+
3
13
  ## [0.4.5] - 2026-03-22
4
14
 
5
15
  ### Fixed
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Client
6
+ class Connection
7
+ attr_reader :name, :transport_type, :config
8
+
9
+ TOOL_CACHE_TTL = 300 # seconds
10
+
11
+ def initialize(name:, transport:, **config)
12
+ @name = name
13
+ @transport_type = transport.to_sym
14
+ @config = config
15
+ @tools_cache = nil
16
+ @tools_cached_at = nil
17
+ @connected = false
18
+ @mutex = Mutex.new
19
+ end
20
+
21
+ def connected?
22
+ @connected
23
+ end
24
+
25
+ def connect
26
+ @mutex.synchronize do
27
+ return if @connected
28
+
29
+ case @transport_type
30
+ when :stdio
31
+ connect_stdio
32
+ when :http, :streamable_http
33
+ connect_http
34
+ else
35
+ raise ArgumentError, "Unknown transport: #{@transport_type}"
36
+ end
37
+ @connected = true
38
+ end
39
+ rescue StandardError
40
+ @connected = false
41
+ raise
42
+ end
43
+
44
+ def disconnect
45
+ @mutex.synchronize do
46
+ @transport&.close if @transport.respond_to?(:close)
47
+ @connected = false
48
+ @tools_cache = nil
49
+ end
50
+ end
51
+
52
+ def tools(force_refresh: false)
53
+ @mutex.synchronize do
54
+ if !force_refresh && @tools_cache && @tools_cached_at &&
55
+ (Time.now - @tools_cached_at) < TOOL_CACHE_TTL
56
+ return @tools_cache
57
+ end
58
+
59
+ @tools_cache = fetch_tools
60
+ @tools_cached_at = Time.now
61
+ @tools_cache
62
+ end
63
+ end
64
+
65
+ def call_tool(name:, arguments: {})
66
+ connect unless connected?
67
+ execute_tool_call(name: name, arguments: arguments)
68
+ end
69
+
70
+ private
71
+
72
+ def connect_stdio
73
+ @transport = { type: :stdio, command: @config[:command], pid: nil }
74
+ end
75
+
76
+ def connect_http
77
+ @transport = { type: :http, url: @config[:url], auth: @config[:auth] }
78
+ end
79
+
80
+ def fetch_tools
81
+ connect unless connected?
82
+ []
83
+ end
84
+
85
+ def execute_tool_call(_name:, _arguments:)
86
+ connect unless connected?
87
+ { content: [], error: false }
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Client
6
+ module Pool
7
+ @connections = {}
8
+ @mutex = Mutex.new
9
+
10
+ module_function
11
+
12
+ def connection_for(server_name)
13
+ @mutex.synchronize do
14
+ return @connections[server_name] if @connections.key?(server_name)
15
+
16
+ config = ServerRegistry.servers[server_name]
17
+ return nil unless config
18
+
19
+ conn = Connection.new(name: server_name, **config.except(:registered_at, :source))
20
+ @connections[server_name] = conn
21
+ end
22
+ end
23
+
24
+ def all_tools
25
+ ServerRegistry.healthy_servers.flat_map do |name, _config|
26
+ conn = connection_for(name)
27
+ next [] unless conn
28
+
29
+ conn.tools.map do |tool|
30
+ tool.merge(source: { type: :mcp, server: name })
31
+ end
32
+ rescue StandardError => e
33
+ Legion::Logging.warn("MCP tool discovery failed for #{name}: #{e.message}") if defined?(Legion::Logging)
34
+ ServerRegistry.mark_unhealthy(name)
35
+ []
36
+ end
37
+ end
38
+
39
+ def reset!
40
+ @mutex.synchronize do
41
+ @connections.each_value { |c| c.disconnect rescue nil } # rubocop:disable Style/RescueModifier
42
+ @connections.clear
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Client
6
+ module ServerRegistry
7
+ @servers = {}
8
+ @health = {}
9
+ @mutex = Mutex.new
10
+
11
+ module_function
12
+
13
+ def load_from_settings(settings_hash)
14
+ @mutex.synchronize do
15
+ settings_hash.each do |name, config|
16
+ @servers[name] = config.merge(registered_at: Time.now, source: :settings)
17
+ @health[name] = { healthy: true, last_check: Time.now }
18
+ end
19
+ end
20
+ end
21
+
22
+ def register(name, **config)
23
+ @mutex.synchronize do
24
+ @servers[name] = config.merge(registered_at: Time.now, source: :dynamic)
25
+ @health[name] = { healthy: true, last_check: Time.now }
26
+ end
27
+ end
28
+
29
+ def deregister(name)
30
+ @mutex.synchronize do
31
+ @servers.delete(name)
32
+ @health.delete(name)
33
+ end
34
+ end
35
+
36
+ def servers
37
+ @mutex.synchronize { @servers.dup }
38
+ end
39
+
40
+ def healthy_servers
41
+ @mutex.synchronize do
42
+ @servers.select do |name, _|
43
+ h = @health[name]
44
+ next true if h.nil? || h[:healthy]
45
+
46
+ cooldown = h[:cooldown] || 60
47
+ if Time.now - (h[:marked_at] || Time.now) >= cooldown
48
+ h[:healthy] = true
49
+ true
50
+ else
51
+ false
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def mark_unhealthy(name, cooldown: 60)
58
+ @mutex.synchronize do
59
+ @health[name] = {
60
+ healthy: false,
61
+ marked_at: Time.now,
62
+ cooldown: cooldown,
63
+ last_check: Time.now
64
+ }
65
+ end
66
+ end
67
+
68
+ def mark_healthy(name)
69
+ @mutex.synchronize do
70
+ @health[name] = { healthy: true, last_check: Time.now }
71
+ end
72
+ end
73
+
74
+ def reset!
75
+ @mutex.synchronize do
76
+ @servers.clear
77
+ @health.clear
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Client
6
+ module_function
7
+
8
+ def boot
9
+ servers = Legion::Settings.dig(:mcp, :servers) rescue nil # rubocop:disable Style/RescueModifier
10
+ return unless servers.is_a?(Hash) && servers.any?
11
+
12
+ ServerRegistry.load_from_settings(servers)
13
+ Legion::Logging.info("MCP Client: #{servers.length} servers registered") if defined?(Legion::Logging)
14
+ end
15
+
16
+ def shutdown
17
+ Pool.reset!
18
+ ServerRegistry.reset!
19
+ end
20
+
21
+ def register(name, **config)
22
+ ServerRegistry.register(name, **config)
23
+ end
24
+
25
+ def deregister(name)
26
+ Pool.reset!
27
+ ServerRegistry.deregister(name)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ require_relative 'client/server_registry'
34
+ require_relative 'client/connection'
35
+ require_relative 'client/pool'
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module MCP
5
+ module Settings
6
+ module_function
7
+
8
+ def defaults
9
+ {
10
+ servers: {},
11
+ overrides: {},
12
+ tool_cache_ttl: 300,
13
+ connect_timeout: 10,
14
+ call_timeout: 30
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module MCP
5
- VERSION = '0.4.5'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
data/lib/legion/mcp.rb CHANGED
@@ -4,9 +4,11 @@ require 'mcp'
4
4
  require 'legion/json'
5
5
  require_relative 'mcp/version'
6
6
 
7
+ require_relative 'mcp/settings'
7
8
  require_relative 'mcp/auth'
8
9
  require_relative 'mcp/tool_governance'
9
10
  require_relative 'mcp/server'
11
+ require_relative 'mcp/client'
10
12
 
11
13
  module Legion
12
14
  module MCP
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -119,6 +119,10 @@ files:
119
119
  - lib/legion/mcp.rb
120
120
  - lib/legion/mcp/auth.rb
121
121
  - lib/legion/mcp/capability_generator.rb
122
+ - lib/legion/mcp/client.rb
123
+ - lib/legion/mcp/client/connection.rb
124
+ - lib/legion/mcp/client/pool.rb
125
+ - lib/legion/mcp/client/server_registry.rb
122
126
  - lib/legion/mcp/cold_start.rb
123
127
  - lib/legion/mcp/context_compiler.rb
124
128
  - lib/legion/mcp/context_guard.rb
@@ -133,6 +137,7 @@ files:
133
137
  - lib/legion/mcp/resources/extension_info.rb
134
138
  - lib/legion/mcp/resources/runner_catalog.rb
135
139
  - lib/legion/mcp/server.rb
140
+ - lib/legion/mcp/settings.rb
136
141
  - lib/legion/mcp/tier_router.rb
137
142
  - lib/legion/mcp/tool_governance.rb
138
143
  - lib/legion/mcp/tools/ask_peer.rb