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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -5
  3. data/app/controllers/action_mcp/application_controller.rb +75 -17
  4. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +8 -8
  5. data/lib/action_mcp/client/base.rb +1 -0
  6. data/lib/action_mcp/client/elicitation.rb +34 -0
  7. data/lib/action_mcp/client/json_rpc_handler.rb +13 -1
  8. data/lib/action_mcp/configuration.rb +9 -0
  9. data/lib/action_mcp/content/resource_link.rb +42 -0
  10. data/lib/action_mcp/json_rpc_handler_base.rb +3 -0
  11. data/lib/action_mcp/prompt.rb +17 -1
  12. data/lib/action_mcp/renderable.rb +18 -0
  13. data/lib/action_mcp/resource_template.rb +18 -2
  14. data/lib/action_mcp/server/active_record_session_store.rb +28 -0
  15. data/lib/action_mcp/server/capabilities.rb +2 -1
  16. data/lib/action_mcp/server/elicitation.rb +64 -0
  17. data/lib/action_mcp/server/json_rpc_handler.rb +14 -2
  18. data/lib/action_mcp/server/memory_session.rb +14 -1
  19. data/lib/action_mcp/server/messaging.rb +10 -6
  20. data/lib/action_mcp/server/solid_mcp_adapter.rb +171 -0
  21. data/lib/action_mcp/server/test_session_store.rb +28 -0
  22. data/lib/action_mcp/server/tools.rb +1 -0
  23. data/lib/action_mcp/server/transport_handler.rb +1 -0
  24. data/lib/action_mcp/server/volatile_session_store.rb +24 -0
  25. data/lib/action_mcp/server.rb +4 -4
  26. data/lib/action_mcp/tagged_stream_logging.rb +26 -5
  27. data/lib/action_mcp/tool.rb +101 -7
  28. data/lib/action_mcp/tool_response.rb +16 -5
  29. data/lib/action_mcp/types/float_array_type.rb +58 -0
  30. data/lib/action_mcp/version.rb +1 -1
  31. data/lib/action_mcp.rb +1 -1
  32. data/lib/generators/action_mcp/install/install_generator.rb +2 -1
  33. data/lib/generators/action_mcp/install/templates/mcp.yml +3 -2
  34. metadata +22 -4
  35. 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
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.55.1"
5
+ VERSION = "0.60.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
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 " - solid_cable : Database-backed adapter (requires solid_cable gem)"
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: solid_cable
110
+ adapter: solid_mcp
111
111
  polling_interval: 0.5.seconds
112
- # connects_to: cable # Optional: specify a different database connection
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.55.1
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.1
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.1
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/solid_cable_adapter.rb
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