actionmcp 0.55.1 → 0.60.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/README.md +11 -5
- data/app/controllers/action_mcp/application_controller.rb +75 -17
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +8 -8
- data/lib/action_mcp/client/base.rb +1 -0
- data/lib/action_mcp/client/elicitation.rb +34 -0
- data/lib/action_mcp/client/json_rpc_handler.rb +13 -1
- data/lib/action_mcp/configuration.rb +9 -0
- data/lib/action_mcp/content/resource_link.rb +42 -0
- data/lib/action_mcp/json_rpc_handler_base.rb +3 -0
- data/lib/action_mcp/prompt.rb +17 -1
- data/lib/action_mcp/renderable.rb +18 -0
- data/lib/action_mcp/resource_template.rb +18 -2
- data/lib/action_mcp/server/active_record_session_store.rb +28 -0
- data/lib/action_mcp/server/capabilities.rb +2 -1
- data/lib/action_mcp/server/elicitation.rb +64 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +14 -2
- data/lib/action_mcp/server/memory_session.rb +14 -1
- data/lib/action_mcp/server/messaging.rb +10 -6
- data/lib/action_mcp/server/solid_mcp_adapter.rb +171 -0
- data/lib/action_mcp/server/test_session_store.rb +28 -0
- data/lib/action_mcp/server/tools.rb +1 -0
- data/lib/action_mcp/server/transport_handler.rb +1 -0
- data/lib/action_mcp/server/volatile_session_store.rb +24 -0
- data/lib/action_mcp/server.rb +4 -4
- data/lib/action_mcp/tagged_stream_logging.rb +26 -5
- data/lib/action_mcp/tool.rb +101 -7
- data/lib/action_mcp/tool_response.rb +16 -5
- data/lib/action_mcp/types/float_array_type.rb +58 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +1 -1
- data/lib/generators/action_mcp/install/install_generator.rb +2 -1
- data/lib/generators/action_mcp/install/templates/mcp.yml +3 -2
- metadata +22 -4
- data/lib/action_mcp/server/solid_cable_adapter.rb +0 -221
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Types
|
5
|
+
# Custom ActiveModel type for handling arrays of floating point numbers
|
6
|
+
class FloatArrayType < ActiveModel::Type::Value
|
7
|
+
def type
|
8
|
+
:float_array
|
9
|
+
end
|
10
|
+
|
11
|
+
def cast(value)
|
12
|
+
return [] if value.nil?
|
13
|
+
return value if value.is_a?(Array) && value.all? { |v| v.is_a?(Float) }
|
14
|
+
|
15
|
+
Array(value).map do |v|
|
16
|
+
case v
|
17
|
+
when Float then v
|
18
|
+
when Numeric then v.to_f
|
19
|
+
when String
|
20
|
+
case v.downcase
|
21
|
+
when "infinity", "+infinity"
|
22
|
+
Float::INFINITY
|
23
|
+
when "-infinity"
|
24
|
+
-Float::INFINITY
|
25
|
+
when "nan"
|
26
|
+
Float::NAN
|
27
|
+
else
|
28
|
+
Float(v) rescue nil
|
29
|
+
end
|
30
|
+
else
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end.compact
|
34
|
+
end
|
35
|
+
|
36
|
+
def serialize(value)
|
37
|
+
cast(value)
|
38
|
+
end
|
39
|
+
|
40
|
+
def deserialize(value)
|
41
|
+
return value if value.is_a?(Array)
|
42
|
+
return [] if value.nil?
|
43
|
+
|
44
|
+
# Handle JSON deserialization
|
45
|
+
if value.is_a?(String)
|
46
|
+
begin
|
47
|
+
parsed = JSON.parse(value)
|
48
|
+
cast(parsed)
|
49
|
+
rescue JSON::ParserError
|
50
|
+
[]
|
51
|
+
end
|
52
|
+
else
|
53
|
+
cast(value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/action_mcp/version.rb
CHANGED
data/lib/action_mcp.rb
CHANGED
@@ -39,7 +39,7 @@ module ActionMCP
|
|
39
39
|
include Logging
|
40
40
|
PROTOCOL_VERSION = "2025-03-26" # Default version
|
41
41
|
CURRENT_VERSION = "2025-03-26" # Current version
|
42
|
-
SUPPORTED_VERSIONS = %w[2025-03-26].freeze
|
42
|
+
SUPPORTED_VERSIONS = %w[2025-06-18 2025-03-26].freeze
|
43
43
|
class << self
|
44
44
|
# Returns a Rack-compatible application for serving MCP requests
|
45
45
|
# This makes ActionMCP.server work similar to ActionCable.server
|
@@ -5,6 +5,7 @@ require "rails/generators"
|
|
5
5
|
module ActionMCP
|
6
6
|
module Generators
|
7
7
|
class InstallGenerator < Rails::Generators::Base
|
8
|
+
namespace "action_mcp:install"
|
8
9
|
source_root File.expand_path("templates", __dir__)
|
9
10
|
|
10
11
|
desc "Install ActionMCP with base classes and configuration"
|
@@ -48,7 +49,7 @@ module ActionMCP
|
|
48
49
|
say "Available adapters:"
|
49
50
|
say " - simple : In-memory adapter for development"
|
50
51
|
say " - test : Test adapter for testing environments"
|
51
|
-
say " -
|
52
|
+
say " - solid_mcp : Database-backed adapter optimized for MCP (requires solid_mcp gem)"
|
52
53
|
say " - redis : Redis-backed adapter (requires redis gem)"
|
53
54
|
say ""
|
54
55
|
say "Next steps:"
|
@@ -107,9 +107,10 @@ production:
|
|
107
107
|
# Choose one of the following adapters:
|
108
108
|
|
109
109
|
# 1. Database-backed adapter (recommended)
|
110
|
-
adapter:
|
110
|
+
adapter: solid_mcp
|
111
111
|
polling_interval: 0.5.seconds
|
112
|
-
|
112
|
+
batch_size: 200 # Number of messages to write in a single batch
|
113
|
+
flush_interval: 0.05 # Seconds between batch flushes
|
113
114
|
|
114
115
|
# Thread pool configuration (optional)
|
115
116
|
min_threads: 10 # Minimum number of threads in the pool
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actionmcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.60.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -43,14 +43,14 @@ dependencies:
|
|
43
43
|
requirements:
|
44
44
|
- - ">="
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: 0.5.
|
46
|
+
version: 0.5.2
|
47
47
|
type: :runtime
|
48
48
|
prerelease: false
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - ">="
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: 0.5.
|
53
|
+
version: 0.5.2
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
55
|
name: multi_json
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
@@ -177,6 +177,20 @@ dependencies:
|
|
177
177
|
- - "~>"
|
178
178
|
- !ruby/object:Gem::Version
|
179
179
|
version: '1.0'
|
180
|
+
- !ruby/object:Gem::Dependency
|
181
|
+
name: json_schemer
|
182
|
+
requirement: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - "~>"
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '2.0'
|
187
|
+
type: :development
|
188
|
+
prerelease: false
|
189
|
+
version_requirements: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - "~>"
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '2.0'
|
180
194
|
description: It offers base classes and helpers for creating MCP applications, making
|
181
195
|
it easier to integrate your Ruby/Rails application with the MCP standard
|
182
196
|
email:
|
@@ -215,6 +229,7 @@ files:
|
|
215
229
|
- lib/action_mcp/client/blueprint.rb
|
216
230
|
- lib/action_mcp/client/catalog.rb
|
217
231
|
- lib/action_mcp/client/collection.rb
|
232
|
+
- lib/action_mcp/client/elicitation.rb
|
218
233
|
- lib/action_mcp/client/json_rpc_handler.rb
|
219
234
|
- lib/action_mcp/client/logging.rb
|
220
235
|
- lib/action_mcp/client/messaging.rb
|
@@ -242,6 +257,7 @@ files:
|
|
242
257
|
- lib/action_mcp/content/base.rb
|
243
258
|
- lib/action_mcp/content/image.rb
|
244
259
|
- lib/action_mcp/content/resource.rb
|
260
|
+
- lib/action_mcp/content/resource_link.rb
|
245
261
|
- lib/action_mcp/content/text.rb
|
246
262
|
- lib/action_mcp/current.rb
|
247
263
|
- lib/action_mcp/current_helpers.rb
|
@@ -275,6 +291,7 @@ files:
|
|
275
291
|
- lib/action_mcp/server/base_messaging.rb
|
276
292
|
- lib/action_mcp/server/capabilities.rb
|
277
293
|
- lib/action_mcp/server/configuration.rb
|
294
|
+
- lib/action_mcp/server/elicitation.rb
|
278
295
|
- lib/action_mcp/server/error_aware.rb
|
279
296
|
- lib/action_mcp/server/error_handling.rb
|
280
297
|
- lib/action_mcp/server/handlers/prompt_handler.rb
|
@@ -294,7 +311,7 @@ files:
|
|
294
311
|
- lib/action_mcp/server/session_store.rb
|
295
312
|
- lib/action_mcp/server/session_store_factory.rb
|
296
313
|
- lib/action_mcp/server/simple_pub_sub.rb
|
297
|
-
- lib/action_mcp/server/
|
314
|
+
- lib/action_mcp/server/solid_mcp_adapter.rb
|
298
315
|
- lib/action_mcp/server/test_session_store.rb
|
299
316
|
- lib/action_mcp/server/tools.rb
|
300
317
|
- lib/action_mcp/server/transport_handler.rb
|
@@ -309,6 +326,7 @@ files:
|
|
309
326
|
- lib/action_mcp/tool_response.rb
|
310
327
|
- lib/action_mcp/tools_registry.rb
|
311
328
|
- lib/action_mcp/transport.rb
|
329
|
+
- lib/action_mcp/types/float_array_type.rb
|
312
330
|
- lib/action_mcp/uri_ambiguity_checker.rb
|
313
331
|
- lib/action_mcp/version.rb
|
314
332
|
- lib/actionmcp.rb
|
@@ -1,221 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "securerandom"
|
4
|
-
require "concurrent/map"
|
5
|
-
require "concurrent/array"
|
6
|
-
require "concurrent/executor/thread_pool_executor"
|
7
|
-
|
8
|
-
module ActionMCP
|
9
|
-
module Server
|
10
|
-
# Mock SolidCable::PubSub for testing
|
11
|
-
class MockSolidCablePubSub
|
12
|
-
attr_reader :subscriptions, :messages
|
13
|
-
|
14
|
-
def initialize(options = {})
|
15
|
-
@options = options
|
16
|
-
@subscriptions = Concurrent::Map.new
|
17
|
-
@messages = Concurrent::Array.new
|
18
|
-
end
|
19
|
-
|
20
|
-
def subscribe(channel, &block)
|
21
|
-
@subscriptions[channel] ||= Concurrent::Array.new
|
22
|
-
@subscriptions[channel] << block
|
23
|
-
end
|
24
|
-
|
25
|
-
def unsubscribe(channel)
|
26
|
-
@subscriptions.delete(channel)
|
27
|
-
end
|
28
|
-
|
29
|
-
def broadcast(channel, message)
|
30
|
-
@messages << { channel: channel, message: message }
|
31
|
-
callbacks = @subscriptions[channel] || []
|
32
|
-
callbacks.each { |callback| callback.call(message) }
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# Adapter for SolidCable PubSub
|
37
|
-
class SolidCableAdapter
|
38
|
-
# Thread pool configuration
|
39
|
-
DEFAULT_MIN_THREADS = 5
|
40
|
-
DEFAULT_MAX_THREADS = 10
|
41
|
-
DEFAULT_MAX_QUEUE = 100
|
42
|
-
DEFAULT_THREAD_TIMEOUT = 60 # seconds
|
43
|
-
|
44
|
-
def initialize(options = {})
|
45
|
-
@options = options
|
46
|
-
@subscriptions = Concurrent::Map.new
|
47
|
-
@channels = Concurrent::Map.new
|
48
|
-
@channel_subscribed = Concurrent::Map.new # Track channel subscription status
|
49
|
-
|
50
|
-
# Initialize thread pool for callbacks
|
51
|
-
pool_options = {
|
52
|
-
min_threads: options["min_threads"] || DEFAULT_MIN_THREADS,
|
53
|
-
max_threads: options["max_threads"] || DEFAULT_MAX_THREADS,
|
54
|
-
max_queue: options["max_queue"] || DEFAULT_MAX_QUEUE,
|
55
|
-
fallback_policy: :caller_runs, # Execute in the caller's thread if queue is full
|
56
|
-
idletime: DEFAULT_THREAD_TIMEOUT
|
57
|
-
}
|
58
|
-
@thread_pool = Concurrent::ThreadPoolExecutor.new(pool_options)
|
59
|
-
|
60
|
-
# Configure SolidCable with options from mcp.yml
|
61
|
-
# The main option we care about is polling_interval
|
62
|
-
pubsub_options = {}
|
63
|
-
|
64
|
-
if @options["polling_interval"]
|
65
|
-
# Convert from ActiveSupport::Duration if needed (e.g., "0.1.seconds")
|
66
|
-
interval = @options["polling_interval"]
|
67
|
-
interval = interval.to_f if interval.respond_to?(:to_f)
|
68
|
-
pubsub_options[:polling_interval] = interval
|
69
|
-
end
|
70
|
-
|
71
|
-
# If there's a connects_to option, pass it along
|
72
|
-
pubsub_options[:connects_to] = @options["connects_to"] if @options["connects_to"]
|
73
|
-
|
74
|
-
# Use mock version for testing or real version in production
|
75
|
-
@solid_cable_pubsub = if defined?(SolidCable) && !testing?
|
76
|
-
SolidCable::PubSub.new(pubsub_options)
|
77
|
-
else
|
78
|
-
MockSolidCablePubSub.new(pubsub_options)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# Subscribe to a channel
|
83
|
-
# @param channel [String] The channel name
|
84
|
-
# @param message_callback [Proc] Callback for received messages
|
85
|
-
# @param success_callback [Proc] Callback for successful subscription
|
86
|
-
# @return [String] Subscription ID
|
87
|
-
def subscribe(channel, message_callback, success_callback = nil)
|
88
|
-
subscription_id = SecureRandom.uuid
|
89
|
-
|
90
|
-
@subscriptions[subscription_id] = {
|
91
|
-
channel: channel,
|
92
|
-
message_callback: message_callback
|
93
|
-
}
|
94
|
-
|
95
|
-
@channels[channel] ||= Concurrent::Array.new
|
96
|
-
@channels[channel] << subscription_id
|
97
|
-
|
98
|
-
# Subscribe to SolidCable only if we haven't already subscribed to this channel
|
99
|
-
unless subscribed_to_solid_cable?(channel)
|
100
|
-
@solid_cable_pubsub.subscribe(channel) do |message|
|
101
|
-
dispatch_message(channel, message)
|
102
|
-
end
|
103
|
-
@channel_subscribed[channel] = true
|
104
|
-
end
|
105
|
-
|
106
|
-
log_subscription_event(channel, "Subscribed", subscription_id)
|
107
|
-
success_callback&.call
|
108
|
-
|
109
|
-
subscription_id
|
110
|
-
end
|
111
|
-
|
112
|
-
# Unsubscribe from a channel
|
113
|
-
# @param channel [String] The channel name
|
114
|
-
# @param callback [Proc] Optional callback for unsubscribe completion
|
115
|
-
def unsubscribe(channel, callback = nil)
|
116
|
-
# Remove our subscriptions
|
117
|
-
subscription_ids = @channels[channel] || []
|
118
|
-
subscription_ids.each do |subscription_id|
|
119
|
-
@subscriptions.delete(subscription_id)
|
120
|
-
end
|
121
|
-
|
122
|
-
@channels.delete(channel)
|
123
|
-
|
124
|
-
# Only unsubscribe from SolidCable if we're actually subscribed
|
125
|
-
if subscribed_to_solid_cable?(channel)
|
126
|
-
@solid_cable_pubsub.unsubscribe(channel)
|
127
|
-
@channel_subscribed.delete(channel)
|
128
|
-
end
|
129
|
-
|
130
|
-
log_subscription_event(channel, "Unsubscribed")
|
131
|
-
callback&.call
|
132
|
-
end
|
133
|
-
|
134
|
-
# Broadcast a message to a channel
|
135
|
-
# @param channel [String] The channel name
|
136
|
-
# @param message [String] The message to broadcast
|
137
|
-
def broadcast(channel, message)
|
138
|
-
@solid_cable_pubsub.broadcast(channel, message)
|
139
|
-
log_broadcast_event(channel, message)
|
140
|
-
end
|
141
|
-
|
142
|
-
# Check if a channel has subscribers
|
143
|
-
# @param channel [String] The channel name
|
144
|
-
# @return [Boolean] True if channel has subscribers
|
145
|
-
def has_subscribers?(channel)
|
146
|
-
subscribers = @channels[channel]
|
147
|
-
return false unless subscribers
|
148
|
-
|
149
|
-
!subscribers.empty?
|
150
|
-
end
|
151
|
-
|
152
|
-
# Check if we're already subscribed to a channel
|
153
|
-
# @param channel [String] The channel name
|
154
|
-
# @return [Boolean] True if we're already subscribed
|
155
|
-
def subscribed_to?(channel)
|
156
|
-
channel_subs = @channels[channel]
|
157
|
-
return false if channel_subs.nil?
|
158
|
-
|
159
|
-
!channel_subs.empty?
|
160
|
-
end
|
161
|
-
|
162
|
-
# Shut down the thread pool gracefully
|
163
|
-
def shutdown
|
164
|
-
@thread_pool.shutdown
|
165
|
-
@thread_pool.wait_for_termination(5) # Wait up to 5 seconds for tasks to complete
|
166
|
-
end
|
167
|
-
|
168
|
-
private
|
169
|
-
|
170
|
-
# Check if we're in a testing environment
|
171
|
-
def testing?
|
172
|
-
defined?(Minitest) || ENV["RAILS_ENV"] == "test"
|
173
|
-
end
|
174
|
-
|
175
|
-
# Check if we're already subscribed to this channel in SolidCable
|
176
|
-
def subscribed_to_solid_cable?(channel)
|
177
|
-
@channel_subscribed[channel] == true
|
178
|
-
end
|
179
|
-
|
180
|
-
def dispatch_message(channel, message)
|
181
|
-
subscription_ids = @channels[channel] || []
|
182
|
-
|
183
|
-
subscription_ids.each do |subscription_id|
|
184
|
-
subscription = @subscriptions[subscription_id]
|
185
|
-
next unless subscription && subscription[:message_callback]
|
186
|
-
|
187
|
-
@thread_pool.post do
|
188
|
-
subscription[:message_callback].call(message)
|
189
|
-
rescue StandardError => e
|
190
|
-
log_error("Error in message callback: #{e.message}\n#{e.backtrace.join("\n")}")
|
191
|
-
end
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
def log_subscription_event(channel, action, subscription_id = nil)
|
196
|
-
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
197
|
-
|
198
|
-
message = "SolidCableAdapter: #{action} channel=#{channel}"
|
199
|
-
message += " subscription_id=#{subscription_id}" if subscription_id
|
200
|
-
|
201
|
-
Rails.logger.debug(message)
|
202
|
-
end
|
203
|
-
|
204
|
-
def log_broadcast_event(channel, message)
|
205
|
-
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
206
|
-
|
207
|
-
# Truncate the message for logging
|
208
|
-
truncated_message = message.to_s[0..100]
|
209
|
-
truncated_message += "..." if message.to_s.length > 100
|
210
|
-
|
211
|
-
Rails.logger.debug("SolidCableAdapter: Broadcasting to channel=#{channel} message=#{truncated_message}")
|
212
|
-
end
|
213
|
-
|
214
|
-
def log_error(message)
|
215
|
-
return unless defined?(Rails) && Rails.respond_to?(:logger)
|
216
|
-
|
217
|
-
Rails.logger.error("SolidCableAdapter: #{message}")
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|