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 +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/legion/mcp/client/connection.rb +92 -0
- data/lib/legion/mcp/client/pool.rb +48 -0
- data/lib/legion/mcp/client/server_registry.rb +83 -0
- data/lib/legion/mcp/client.rb +35 -0
- data/lib/legion/mcp/settings.rb +19 -0
- data/lib/legion/mcp/version.rb +1 -1
- data/lib/legion/mcp.rb +2 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2eff77018e5f5162f17ce2582bee76bff4e78d73e3129c9cf3adac4f40922bd1
|
|
4
|
+
data.tar.gz: fbc6a1ad1463750b1381a8d48e56b36c78a2166fd7514db74e0e1cba7a904df0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/legion/mcp/version.rb
CHANGED
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
|
+
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
|