actionmcp 0.60.1 → 0.70.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: 2384a0cea84e17ca34e3c46e4029e2dc008561ebeeb8e52f15cb05c3fb5f5eb4
4
- data.tar.gz: ccba3d9432ae0be54a4950e2f5d357c55fdd39cec2766253e2b3093dba9eedd8
3
+ metadata.gz: 5b498d3bdd7cde670eef99ba3458e4af3808ce1eaf2cb29c369bdaa3048a9098
4
+ data.tar.gz: a3f0f8db133018b5f9a2ee822f35a59c0cbf79301be7b69c894ef4daa2f99472
5
5
  SHA512:
6
- metadata.gz: e7ed0c8b71d4211dc48388a7544c17ecb9ff674faad9274cd1225cb3e3a65f76bcb546c2c2353882a1ade9016670659fed38aa1c90aa94e17479149012e1be01
7
- data.tar.gz: add51e3d310ec89d96bef2767008bc4be9f9bddfeda60ca826d6e9168485771ebf61c03e1011c889d0a57fb4ff3d7bb0e0f5c5046cb35ca69a5285d647fe71d2
6
+ metadata.gz: 4880debf4726112a664348076862e92c92578c9d4cafd6a0ac923944e37d5c6929bab0b3779bcc322fd478b19f57635f629be76100e9b3254b4fd3895c3eff13
7
+ data.tar.gz: dba72df2677959588c73431c99cd7a3a3d176ac3fcbfea99d5ea2c53e2ff96aac228a5a46d5f3dc8c7b1f79a9f2cb1467d0a784236d613deeddb135ddcf300bd
data/README.md CHANGED
@@ -237,30 +237,12 @@ module Tron
237
237
  config.action_mcp.version = "1.2.3" # defaults to "0.0.1"
238
238
  config.action_mcp.logging_enabled = true # defaults to true
239
239
  config.action_mcp.logging_level = :info # defaults to :info, can be :debug, :info, :warn, :error, :fatal
240
- config.action_mcp.vibed_ignore_version = false # defaults to false, set to true to ignore client protocol version mismatches
241
240
  end
242
241
  end
243
242
  ```
244
243
 
245
244
  For dynamic versioning, consider adding the `rails_app_version` gem.
246
245
 
247
- ### Protocol Version Compatibility
248
-
249
- By default, ActionMCP requires clients to use the exact protocol version supported by the server (currently "2025-03-26"). If the client specifies a different version during initialization, the request will be rejected with an error.
250
-
251
- To support clients with incompatible protocol versions, you can enable the `vibed_ignore_version` option:
252
-
253
- ```ruby
254
- # In config/application.rb or an initializer
255
- Rails.application.config.action_mcp.vibed_ignore_version = true
256
- ```
257
-
258
- When enabled, the server will ignore protocol version mismatches from clients and always use the latest supported version. This is useful for:
259
- - Development environments with older client libraries
260
- - Supporting clients that cannot be easily updated
261
- - Situations where protocol differences are minor and known to be compatible
262
-
263
- > **Note:** Using `vibed_ignore_version = true` in production is not recommended as it may lead to unexpected behavior if clients rely on specific protocol features that differ between versions.
264
246
 
265
247
  ### PubSub Configuration
266
248
 
@@ -12,6 +12,7 @@ module ActionMCP
12
12
  include ActionController::Live
13
13
  include ActionController::Instrumentation
14
14
 
15
+
15
16
  # Provides the ActionMCP::Session for the current request.
16
17
  # Handles finding existing sessions via header/param or initializing a new one.
17
18
  # Specific controllers/handlers might need to enforce session ID presence based on context.
@@ -143,7 +144,7 @@ module ActionMCP
143
144
  # @route POST /mcp
144
145
  def create
145
146
  unless post_accept_headers_valid?
146
- id = extract_jsonrpc_id_from_params
147
+ id = extract_jsonrpc_id_from_request
147
148
  return render_not_acceptable(post_accept_headers_error_message, id)
148
149
  end
149
150
 
@@ -157,8 +158,7 @@ module ActionMCP
157
158
  session = mcp_session
158
159
 
159
160
  # Validate MCP-Protocol-Version header for non-initialize requests
160
- # Temporarily disabled to debug session issues
161
- # return unless validate_protocol_version_header
161
+ return unless validate_protocol_version_header
162
162
 
163
163
  unless is_initialize_request
164
164
  if session_initially_missing
@@ -228,18 +228,26 @@ module ActionMCP
228
228
  # Skip validation for initialize requests
229
229
  return true if check_if_initialize_request(jsonrpc_params)
230
230
 
231
- header_version = request.headers["MCP-Protocol-Version"]
231
+ # Check for both case variations of the header (spec uses MCP-Protocol-Version)
232
+ header_version = request.headers["MCP-Protocol-Version"] || request.headers["mcp-protocol-version"]
232
233
  session = mcp_session
233
234
 
234
- # If header is missing, assume 2025-03-26 for backward compatibility
235
+ # If header is missing, assume 2025-03-26 for backward compatibility as per spec
235
236
  if header_version.nil?
236
- Rails.logger.debug "MCP-Protocol-Version header missing, assuming 2025-03-26 for backward compatibility"
237
+ ActionMCP.logger.debug "MCP-Protocol-Version header missing, assuming 2025-03-26 for backward compatibility"
237
238
  return true
238
239
  end
239
240
 
241
+ # Handle array values (take the last one as per TypeScript SDK)
242
+ if header_version.is_a?(Array)
243
+ header_version = header_version.last
244
+ end
245
+
240
246
  # Check if the header version is supported
241
247
  unless ActionMCP::SUPPORTED_VERSIONS.include?(header_version)
242
- render_bad_request("Unsupported MCP-Protocol-Version: #{header_version}")
248
+ supported_versions = ActionMCP::SUPPORTED_VERSIONS.join(", ")
249
+ ActionMCP.logger.warn "Unsupported MCP-Protocol-Version: #{header_version}. Supported versions: #{supported_versions}"
250
+ render_protocol_version_error("Unsupported MCP-Protocol-Version: #{header_version}. Supported versions: #{supported_versions}")
243
251
  return false
244
252
  end
245
253
 
@@ -247,12 +255,13 @@ module ActionMCP
247
255
  if session && session.initialized?
248
256
  negotiated_version = session.protocol_version
249
257
  if header_version != negotiated_version
250
- Rails.logger.warn "MCP-Protocol-Version mismatch: header=#{header_version}, negotiated=#{negotiated_version}"
251
- render_bad_request("MCP-Protocol-Version header (#{header_version}) does not match negotiated version (#{negotiated_version})")
258
+ ActionMCP.logger.warn "MCP-Protocol-Version mismatch: header=#{header_version}, negotiated=#{negotiated_version}"
259
+ render_protocol_version_error("MCP-Protocol-Version header (#{header_version}) does not match negotiated version (#{negotiated_version})")
252
260
  return false
253
261
  end
254
262
  end
255
263
 
264
+ ActionMCP.logger.debug "MCP-Protocol-Version header validation passed: #{header_version}"
256
265
  true
257
266
  end
258
267
 
@@ -260,12 +269,12 @@ module ActionMCP
260
269
  # Note: This doesn't save the new session; that happens upon first use or explicitly.
261
270
  def find_or_initialize_session
262
271
  session_id = extract_session_id
272
+ session_store = ActionMCP::Server.session_store
273
+
263
274
  if session_id
264
- session = Server.session_store.load_session(session_id)
265
- # Session protocol version is set during initialization and should not be overridden
266
- session
275
+ session_store.load_session(session_id)
267
276
  else
268
- Server.session_store.create_session(nil, protocol_version: ActionMCP::DEFAULT_PROTOCOL_VERSION)
277
+ session_store.create_session(nil, protocol_version: ActionMCP::DEFAULT_PROTOCOL_VERSION)
269
278
  end
270
279
  end
271
280
 
@@ -415,6 +424,12 @@ module ActionMCP
415
424
  render json: { jsonrpc: "2.0", id: id, error: { code: -32_600, message: message } }
416
425
  end
417
426
 
427
+ # Renders a 400 Bad Request response for protocol version errors as per MCP spec
428
+ def render_protocol_version_error(message = "Protocol Version Error", id = nil)
429
+ id ||= extract_jsonrpc_id_from_request
430
+ render json: { jsonrpc: "2.0", id: id, error: { code: -32_000, message: message } }, status: :bad_request
431
+ end
432
+
418
433
  # Renders a 404 Not Found response with a JSON-RPC-like error structure.
419
434
  def render_not_found(message = "Not Found", id = nil)
420
435
  id ||= extract_jsonrpc_id_from_request
@@ -4,17 +4,17 @@
4
4
  #
5
5
  # Table name: action_mcp_session_messages
6
6
  #
7
- # id :bigint not null, primary key
8
- # direction(The message recipient) :string default("client"), not null
9
- # is_ping(Whether the message is a ping) :boolean default(FALSE), not null
10
- # message_json :json
11
- # message_type(The type of the message) :string not null
12
- # request_acknowledged :boolean default(FALSE), not null
13
- # request_cancelled :boolean default(FALSE), not null
14
- # created_at :datetime not null
15
- # updated_at :datetime not null
16
- # jsonrpc_id :string
17
- # session_id :string not null
7
+ # id :bigint not null, primary key
8
+ # direction(The message recipient) :string default("client"), not null
9
+ # is_ping :boolean default(FALSE), not null
10
+ # message_json :json
11
+ # message_type :string not null
12
+ # request_acknowledged :boolean default(FALSE), not null
13
+ # request_cancelled :boolean default(FALSE), not null
14
+ # created_at :datetime not null
15
+ # updated_at :datetime not null
16
+ # jsonrpc_id :string
17
+ # session_id :string not null
18
18
  #
19
19
  # Indexes
20
20
  #
@@ -22,7 +22,7 @@
22
22
  #
23
23
  # Foreign Keys
24
24
  #
25
- # fk_action_mcp_session_messages_session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade ON UPDATE => cascade
25
+ # fk_rails_... (session_id => action_mcp_sessions.id) ON DELETE => cascade ON UPDATE => cascade
26
26
  #
27
27
  module ActionMCP
28
28
  class Session
@@ -75,9 +75,7 @@ module ActionMCP
75
75
  before_create :set_server_info, if: -> { role == "server" }
76
76
  before_create :set_server_capabilities, if: -> { role == "server" }
77
77
 
78
- validates :protocol_version, inclusion: { in: ActionMCP::SUPPORTED_VERSIONS }, allow_nil: true, unless: lambda {
79
- ActionMCP.configuration.vibed_ignore_version
80
- }
78
+ validates :protocol_version, inclusion: { in: ActionMCP::SUPPORTED_VERSIONS }, allow_nil: true
81
79
 
82
80
  def close!
83
81
  dummy_callback = ->(*) { } # this callback seem broken
@@ -115,8 +113,6 @@ module ActionMCP
115
113
  end
116
114
 
117
115
  def set_protocol_version(version)
118
- # If vibed_ignore_version is true, always use the latest supported version
119
- version = PROTOCOL_VERSION if ActionMCP.configuration.vibed_ignore_version
120
116
  update(protocol_version: version)
121
117
  end
122
118
 
@@ -311,29 +307,58 @@ module ActionMCP
311
307
 
312
308
  # Get registered items for this session
313
309
  def registered_tools
314
- (self.tool_registry || []).filter_map do |tool_name|
315
- ActionMCP::ToolsRegistry.find(tool_name)
316
- rescue StandardError
317
- nil
310
+ # Special case: ['*'] means use all available tools dynamically
311
+ if tool_registry == [ "*" ]
312
+ # filtered_tools returns a RegistryScope with Item objects, need to extract the klass
313
+ ActionMCP.configuration.filtered_tools.map(&:klass)
314
+ else
315
+ (self.tool_registry || []).filter_map do |tool_name|
316
+ ActionMCP::ToolsRegistry.find(tool_name)
317
+ rescue StandardError
318
+ nil
319
+ end
318
320
  end
319
321
  end
320
322
 
321
323
  def registered_prompts
322
- (self.prompt_registry || []).filter_map do |prompt_name|
323
- ActionMCP::PromptsRegistry.find(prompt_name)
324
- rescue StandardError
325
- nil
324
+ if prompt_registry == [ "*" ]
325
+ # filtered_prompts returns a RegistryScope with Item objects, need to extract the klass
326
+ ActionMCP.configuration.filtered_prompts.map(&:klass)
327
+ else
328
+ (self.prompt_registry || []).filter_map do |prompt_name|
329
+ ActionMCP::PromptsRegistry.find(prompt_name)
330
+ rescue StandardError
331
+ nil
332
+ end
326
333
  end
327
334
  end
328
335
 
329
336
  def registered_resource_templates
330
- (self.resource_registry || []).filter_map do |template_name|
331
- ActionMCP::ResourceTemplatesRegistry.find(template_name)
332
- rescue StandardError
333
- nil
337
+ if resource_registry == [ "*" ]
338
+ # filtered_resources returns a RegistryScope with Item objects, need to extract the klass
339
+ ActionMCP.configuration.filtered_resources.map(&:klass)
340
+ else
341
+ (self.resource_registry || []).filter_map do |template_name|
342
+ ActionMCP::ResourceTemplatesRegistry.find(template_name)
343
+ rescue StandardError
344
+ nil
345
+ end
334
346
  end
335
347
  end
336
348
 
349
+ # Helper methods to check if using all capabilities
350
+ def uses_all_tools?
351
+ tool_registry == [ "*" ]
352
+ end
353
+
354
+ def uses_all_prompts?
355
+ prompt_registry == [ "*" ]
356
+ end
357
+
358
+ def uses_all_resources?
359
+ resource_registry == [ "*" ]
360
+ end
361
+
337
362
  # OAuth Session Management
338
363
  # Required by MCP 2025-03-26 specification for session binding
339
364
 
@@ -443,10 +468,10 @@ module ActionMCP
443
468
  end
444
469
 
445
470
  def initialize_registries
446
- # Start with default registries from configuration
447
- self.tool_registry = ActionMCP.configuration.filtered_tools.map(&:name)
448
- self.prompt_registry = ActionMCP.configuration.filtered_prompts.map(&:name)
449
- self.resource_registry = ActionMCP.configuration.filtered_resources.map(&:name)
471
+ # Default to using all available capabilities with '*'
472
+ self.tool_registry = [ "*" ]
473
+ self.prompt_registry = [ "*" ]
474
+ self.resource_registry = [ "*" ]
450
475
  end
451
476
 
452
477
  def normalize_name(class_or_name, type)
@@ -19,7 +19,7 @@ module ActionMCP
19
19
  end
20
20
 
21
21
  # Convert to hash format expected by MCP protocol
22
- def to_h
22
+ def to_h(options = nil)
23
23
  if @is_error
24
24
  JSON_RPC::JsonRpcError.new(@symbol, message: @error_message, data: @error_data).to_h
25
25
  else
@@ -49,6 +49,8 @@ module ActionMCP
49
49
  # @return [void]
50
50
  def self.abstract!
51
51
  self.abstract_capability = true
52
+ # Unregister from the appropriate registry if already registered
53
+ unregister_from_registry
52
54
  end
53
55
 
54
56
  # Returns whether this tool is abstract.
@@ -35,12 +35,12 @@ module ActionMCP
35
35
 
36
36
  def handle_notification(notification)
37
37
  # Handle server notifications to client
38
- puts "\e[33mReceived notification: #{notification.method}\e[0m"
38
+ client.log_debug("Received notification: #{notification.method}")
39
39
  end
40
40
 
41
41
  def handle_response(response)
42
42
  # Handle server responses to client requests
43
- puts "\e[32mReceived response: #{response.id} - #{response.result ? 'success' : 'error'}\e[0m"
43
+ client.log_debug("Received response: #{response.id} - #{response.result ? 'success' : 'error'}")
44
44
  end
45
45
 
46
46
  protected
@@ -60,7 +60,7 @@ module ActionMCP
60
60
  else
61
61
  common_result = handle_common_methods(rpc_method, id, params)
62
62
  if common_result.nil?
63
- puts "\e[31mUnknown server method: #{rpc_method} #{id} #{params}\e[0m"
63
+ client.log_warn("Unknown server method: #{rpc_method} #{id} #{params}")
64
64
  end
65
65
  end
66
66
  end
@@ -94,19 +94,19 @@ module ActionMCP
94
94
  def process_notifications(rpc_method, params)
95
95
  case rpc_method
96
96
  when "notifications/resources/updated" # Resource update notification
97
- puts "\e[31m Resource #{params['uri']} was updated\e[0m"
97
+ client.log_debug("Resource #{params['uri']} was updated")
98
98
  # Handle resource update notification
99
99
  # TODO: fetch updated resource or mark it as stale
100
100
  when "notifications/tools/list_changed" # Tool list change notification
101
- puts "\e[31m Tool list has changed\e[0m"
101
+ client.log_debug("Tool list has changed")
102
102
  # Handle tool list change notification
103
103
  # TODO: fetch new tools or mark them as stale
104
104
  when "notifications/prompts/list_changed" # Prompt list change notification
105
- puts "\e[31m Prompt list has changed\e[0m"
105
+ client.log_debug("Prompt list has changed")
106
106
  # Handle prompt list change notification
107
107
  # TODO: fetch new prompts or mark them as stale
108
108
  when "notifications/resources/list_changed" # Resource list change notification
109
- puts "\e[31m Resource list has changed\e[0m"
109
+ client.log_debug("Resource list has changed")
110
110
  # Handle resource list change notification
111
111
  # TODO: fetch new resources or mark them as stale
112
112
  else
@@ -153,12 +153,12 @@ module ActionMCP
153
153
  return true
154
154
  end
155
155
 
156
- puts "\e[31mUnknown response: #{id} #{result}\e[0m"
156
+ client.log_warn("Unknown response: #{id} #{result}")
157
157
  end
158
158
 
159
159
  def process_error(id, error)
160
160
  # Do something ?
161
- puts "\e[31mUnknown error: #{id} #{error}\e[0m"
161
+ client.log_error("Unknown error: #{id} #{error}")
162
162
  end
163
163
 
164
164
  def handle_initialize_response(request_id, result)
@@ -33,8 +33,6 @@ module ActionMCP
33
33
  :sse_heartbeat_interval,
34
34
  :post_response_preference, # :json or :sse
35
35
  :protocol_version,
36
- # --- VibedIgnoreVersion Option ---
37
- :vibed_ignore_version,
38
36
  # --- SSE Resumability Options ---
39
37
  :sse_event_retention_period,
40
38
  :max_stored_sse_events,
@@ -68,7 +66,6 @@ module ActionMCP
68
66
  @sse_heartbeat_interval = 30
69
67
  @post_response_preference = :json
70
68
  @protocol_version = "2025-03-26" # Default to legacy for backwards compatibility
71
- @vibed_ignore_version = false
72
69
 
73
70
  # Resumability defaults
74
71
  @sse_event_retention_period = 15.minutes
@@ -178,15 +175,25 @@ module ActionMCP
178
175
  # Returns capabilities based on active profile
179
176
  def capabilities
180
177
  capabilities = {}
178
+ profile = @profiles[active_profile]
181
179
 
182
- # Only include capabilities if the corresponding filtered registry is non-empty
183
- capabilities[:tools] = { listChanged: @list_changed } if filtered_tools.any?
180
+ # Check profile configuration instead of registry contents
181
+ # If profile includes tools (either "all" or specific tools), advertise tools capability
182
+ if profile && profile[:tools] && profile[:tools].any?
183
+ capabilities[:tools] = { listChanged: @list_changed }
184
+ end
184
185
 
185
- capabilities[:prompts] = { listChanged: @list_changed } if filtered_prompts.any?
186
+ # If profile includes prompts, advertise prompts capability
187
+ if profile && profile[:prompts] && profile[:prompts].any?
188
+ capabilities[:prompts] = { listChanged: @list_changed }
189
+ end
186
190
 
187
191
  capabilities[:logging] = {} if @logging_enabled
188
192
 
189
- capabilities[:resources] = { subscribe: @resources_subscribe } if filtered_resources.any?
193
+ # If profile includes resources, advertise resources capability
194
+ if profile && profile[:resources] && profile[:resources].any?
195
+ capabilities[:resources] = { subscribe: @resources_subscribe }
196
+ end
190
197
 
191
198
  capabilities[:elicitation] = {} if @elicitation_enabled
192
199
 
@@ -214,6 +221,20 @@ module ActionMCP
214
221
  @resources_subscribe = options[:resources_subscribe] unless options[:resources_subscribe].nil?
215
222
  end
216
223
 
224
+ def eager_load_if_needed
225
+ profile = @profiles[active_profile]
226
+ return unless profile
227
+
228
+ # Check if any component type includes "all"
229
+ needs_eager_load = profile[:tools]&.include?("all") ||
230
+ profile[:prompts]&.include?("all") ||
231
+ profile[:resources]&.include?("all")
232
+
233
+ if needs_eager_load
234
+ ensure_mcp_components_loaded
235
+ end
236
+ end
237
+
217
238
  private
218
239
 
219
240
  def default_profiles
@@ -303,6 +324,41 @@ module ActionMCP
303
324
  rescue LoadError
304
325
  false
305
326
  end
327
+
328
+ def ensure_mcp_components_loaded
329
+ # Only load if we haven't loaded yet - but in development, always reload
330
+ return if @mcp_components_loaded && !Rails.env.development?
331
+
332
+ # Use Zeitwerk eager loading if available (in to_prepare phase)
333
+ mcp_path = Rails.root.join("app/mcp")
334
+ if mcp_path.exist? && Rails.autoloaders.main.respond_to?(:eager_load_dir)
335
+ # This will trigger all inherited hooks properly
336
+ Rails.autoloaders.main.eager_load_dir(mcp_path)
337
+ elsif mcp_path.exist?
338
+ # Fallback for initialization phase - use require_dependency
339
+ # Load base classes first in specific order
340
+ base_files = [
341
+ mcp_path.join("application_gateway.rb"),
342
+ mcp_path.join("tools/application_mcp_tool.rb"),
343
+ mcp_path.join("prompts/application_mcp_prompt.rb"),
344
+ mcp_path.join("resource_templates/application_mcp_res_template.rb"),
345
+ # Load ArithmeticTool before other tools that inherit from it
346
+ mcp_path.join("tools/arithmetic_tool.rb")
347
+ ]
348
+
349
+ base_files.each do |file|
350
+ require_dependency file.to_s if file.exist?
351
+ end
352
+
353
+ # Then load all other files
354
+ Dir.glob(mcp_path.join("**/*.rb")).sort.each do |file|
355
+ # Skip base classes we already loaded
356
+ next if base_files.any? { |base| file == base.to_s }
357
+ require_dependency file
358
+ end
359
+ end
360
+ @mcp_components_loaded = true unless Rails.env.development?
361
+ end
306
362
  end
307
363
 
308
364
  class << self
@@ -18,8 +18,33 @@ module ActionMCP
18
18
  # Provide a configuration namespace for ActionMCP
19
19
  config.action_mcp = ActionMCP.configuration
20
20
 
21
+ # Create the ActiveSupport load hooks
22
+ ActiveSupport.on_load(:action_mcp_tool) do
23
+ # Register the tool when it's loaded
24
+ ActionMCP::ToolsRegistry.register(self) unless abstract?
25
+ end
26
+
27
+ ActiveSupport.on_load(:action_mcp_prompt) do
28
+ # Register the prompt when it's loaded
29
+ ActionMCP::PromptsRegistry.register(self) unless abstract?
30
+ end
31
+
32
+ ActiveSupport.on_load(:action_mcp_resource_template) do
33
+ # Register the resource template when it's loaded
34
+ ActionMCP::ResourceTemplatesRegistry.register(self) unless abstract?
35
+ end
36
+
21
37
  config.to_prepare do
22
- ActionMCP::ResourceTemplate.registered_templates.clear
38
+ # Only clear registries if we're in development mode
39
+ if Rails.env.development?
40
+ ActionMCP::ResourceTemplate.registered_templates.clear
41
+ ActionMCP::ToolsRegistry.clear!
42
+ ActionMCP::PromptsRegistry.clear!
43
+ end
44
+
45
+ # Eager load MCP components if profile includes "all"
46
+ # This runs after Zeitwerk is fully set up
47
+ ActionMCP.configuration.eager_load_if_needed
23
48
  end
24
49
 
25
50
  config.middleware.use JSONRPC_Rails::Middleware::Validator, [ "/" ]
@@ -27,6 +27,7 @@ module ActionMCP
27
27
  #
28
28
  # @return [String] The default prompt name.
29
29
  def self.default_prompt_name
30
+ return "" if name.nil?
30
31
  name.demodulize.underscore.sub(/_prompt$/, "")
31
32
  end
32
33
 
@@ -37,6 +38,19 @@ module ActionMCP
37
38
  :prompt
38
39
  end
39
40
 
41
+ def unregister_from_registry
42
+ ActionMCP::PromptsRegistry.unregister(self) if ActionMCP::PromptsRegistry.items.values.include?(self)
43
+ end
44
+
45
+ # Hook called when a class inherits from Prompt
46
+ def inherited(subclass)
47
+ super
48
+ # Run the ActiveSupport load hook when a prompt is defined
49
+ subclass.class_eval do
50
+ ActiveSupport.run_load_hooks(:action_mcp_prompt, subclass)
51
+ end
52
+ end
53
+
40
54
  # Sets or retrieves the _meta field
41
55
  def meta(data = nil)
42
56
  if data
@@ -11,11 +11,25 @@ module ActionMCP
11
11
  #
12
12
  # @return [Hash] A hash of registered items.
13
13
  def items
14
- @items = item_klass.descendants.each_with_object({}) do |klass, hash|
15
- next if klass.abstract?
14
+ @items ||= {}
15
+ end
16
+
17
+ # Register an item explicitly
18
+ #
19
+ # @param klass [Class] The class to register
20
+ # @return [void]
21
+ def register(klass)
22
+ return if klass.abstract?
23
+
24
+ items[klass.capability_name] = klass
25
+ end
16
26
 
17
- hash[klass.capability_name] = klass
18
- end
27
+ # Unregister an item
28
+ #
29
+ # @param klass [Class] The class to unregister
30
+ # @return [void]
31
+ def unregister(klass)
32
+ items.delete(klass.capability_name)
19
33
  end
20
34
 
21
35
  # Retrieve an item by name.
@@ -44,6 +58,13 @@ module ActionMCP
44
58
  RegistryScope.new(items)
45
59
  end
46
60
 
61
+ # Clears the registry cache.
62
+ #
63
+ # @return [void]
64
+ def clear!
65
+ @items = nil
66
+ end
67
+
47
68
  private
48
69
 
49
70
  # Helper to determine if an item is abstract.
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ # Manages resource resolution results and errors for ResourceTemplate operations.
5
+ # Unlike ToolResponse, ResourceResponse only uses JSON-RPC protocol errors per MCP spec.
6
+ class ResourceResponse < BaseResponse
7
+ attr_reader :contents
8
+
9
+ delegate :empty?, :size, :each, :find, :map, to: :contents
10
+
11
+ def initialize
12
+ super
13
+ @contents = []
14
+ end
15
+
16
+ # Add a resource content item to the response
17
+ def add_content(content)
18
+ @contents << content
19
+ content # Return the content for chaining
20
+ end
21
+
22
+ # Add multiple content items
23
+ def add_contents(contents_array)
24
+ @contents.concat(contents_array)
25
+ self
26
+ end
27
+
28
+ # Mark as error with ResourceTemplate-specific error types
29
+ def mark_as_template_not_found!(uri)
30
+ mark_as_error!(
31
+ :invalid_params,
32
+ message: "Resource template not found for URI: #{uri}",
33
+ data: { uri: uri, error_type: "TEMPLATE_NOT_FOUND" }
34
+ )
35
+ end
36
+
37
+ def mark_as_parameter_validation_failed!(missing_params, uri)
38
+ mark_as_error!(
39
+ :invalid_params,
40
+ message: "Required parameters missing: #{missing_params.join(', ')}",
41
+ data: {
42
+ uri: uri,
43
+ missing_parameters: missing_params,
44
+ error_type: "PARAMETER_VALIDATION_FAILED"
45
+ }
46
+ )
47
+ end
48
+
49
+ def mark_as_resolution_failed!(uri, reason = nil)
50
+ message = "Resource resolution failed for URI: #{uri}"
51
+ message += " - #{reason}" if reason
52
+
53
+ mark_as_error!(
54
+ :internal_error,
55
+ message: message,
56
+ data: {
57
+ uri: uri,
58
+ reason: reason,
59
+ error_type: "RESOLUTION_FAILED"
60
+ }
61
+ )
62
+ end
63
+
64
+ def mark_as_callback_aborted!(uri)
65
+ mark_as_error!(
66
+ :internal_error,
67
+ message: "Resource resolution was aborted by callback chain",
68
+ data: {
69
+ uri: uri,
70
+ error_type: "CALLBACK_ABORTED"
71
+ }
72
+ )
73
+ end
74
+
75
+ def mark_as_not_found!(uri)
76
+ # Use method_not_found for resource not found (closest standard JSON-RPC error)
77
+ mark_as_error!(
78
+ :method_not_found, # -32601 - closest standard error for "not found"
79
+ message: "Resource not found",
80
+ data: { uri: uri }
81
+ )
82
+ end
83
+
84
+ # Implementation of build_success_hash for ResourceResponse
85
+ def build_success_hash
86
+ {
87
+ contents: @contents.map(&:to_h)
88
+ }
89
+ end
90
+
91
+ # Implementation of compare_with_same_class for ResourceResponse
92
+ def compare_with_same_class(other)
93
+ contents == other.contents && is_error == other.is_error
94
+ end
95
+
96
+ # Implementation of hash_components for ResourceResponse
97
+ def hash_components
98
+ [ contents, is_error ]
99
+ end
100
+
101
+ # Pretty print for better debugging
102
+ def inspect
103
+ if is_error
104
+ "#<#{self.class.name} error: #{@error_message}>"
105
+ else
106
+ "#<#{self.class.name} contents: #{contents.size} items>"
107
+ end
108
+ end
109
+ end
110
+ end
@@ -26,6 +26,8 @@ module ActionMCP
26
26
 
27
27
  def abstract!
28
28
  @abstract = true
29
+ # Unregister from the appropriate registry if already registered
30
+ ActionMCP::ResourceTemplatesRegistry.unregister(self) if ActionMCP::ResourceTemplatesRegistry.items.values.include?(self)
29
31
  end
30
32
 
31
33
  def inherited(subclass)
@@ -33,6 +35,11 @@ module ActionMCP
33
35
  subclass.instance_variable_set(:@abstract, false)
34
36
  # Create a copy of validation requirements for subclasses
35
37
  subclass.instance_variable_set(:@required_parameters, [])
38
+
39
+ # Run the ActiveSupport load hook when a resource template is defined
40
+ subclass.class_eval do
41
+ ActiveSupport.run_load_hooks(:action_mcp_resource_template, subclass)
42
+ end
36
43
  end
37
44
 
38
45
  # Track required parameters for validation
@@ -109,6 +116,7 @@ module ActionMCP
109
116
  end
110
117
 
111
118
  def capability_name
119
+ return "" if name.nil?
112
120
  @capability_name ||= name.demodulize.underscore.sub(/_template$/, "")
113
121
  end
114
122
 
@@ -245,9 +253,29 @@ module ActionMCP
245
253
  end
246
254
 
247
255
  def call
248
- run_callbacks :resolve do
249
- resolve
256
+ @response = ResourceResponse.new
257
+
258
+ # Validate parameters first
259
+ unless valid?
260
+ missing_params = errors.full_messages
261
+ @response.mark_as_parameter_validation_failed!(missing_params, "template://#{self.class.name}")
262
+ return @response
263
+ end
264
+
265
+ begin
266
+ run_callbacks :resolve do
267
+ result = resolve
268
+ if result.nil?
269
+ @response.mark_as_not_found!("template://#{self.class.name}")
270
+ else
271
+ @response.add_content(result)
272
+ end
273
+ end
274
+ rescue StandardError => e
275
+ @response.mark_as_resolution_failed!("template://#{self.class.name}", e.message)
250
276
  end
277
+
278
+ @response
251
279
  end
252
280
  end
253
281
  end
@@ -18,7 +18,7 @@ module ActionMCP
18
18
  unless client_protocol_version.is_a?(String) && client_protocol_version.present?
19
19
  return send_jsonrpc_error(request_id, :invalid_params, "Missing or invalid 'protocolVersion'")
20
20
  end
21
- unless ActionMCP.configuration.vibed_ignore_version || ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
21
+ unless ActionMCP::SUPPORTED_VERSIONS.include?(client_protocol_version)
22
22
  error_message = "Unsupported protocol version. Client requested '#{client_protocol_version}' but server supports #{ActionMCP::SUPPORTED_VERSIONS.join(', ')}"
23
23
  error_data = {
24
24
  supported: ActionMCP::SUPPORTED_VERSIONS,
@@ -44,11 +44,7 @@ module ActionMCP
44
44
 
45
45
  # Return existing session info
46
46
  capabilities_payload = existing_session.server_capabilities_payload
47
- capabilities_payload[:protocolVersion] = if ActionMCP.configuration.vibed_ignore_version
48
- ActionMCP::LATEST_VERSION
49
- else
50
- client_protocol_version
51
- end
47
+ capabilities_payload[:protocolVersion] = client_protocol_version
52
48
  return send_jsonrpc_response(request_id, result: capabilities_payload)
53
49
  else
54
50
  Rails.logger.warn("Session #{session_id} not found or not initialized, creating new session")
@@ -65,11 +61,7 @@ module ActionMCP
65
61
  end
66
62
 
67
63
  capabilities_payload = session.server_capabilities_payload
68
- capabilities_payload[:protocolVersion] = if ActionMCP.configuration.vibed_ignore_version
69
- ActionMCP::LATEST_VERSION
70
- else
71
- client_protocol_version
72
- end
64
+ capabilities_payload[:protocolVersion] = client_protocol_version
73
65
 
74
66
  send_jsonrpc_response(request_id, result: capabilities_payload)
75
67
  end
@@ -182,7 +182,6 @@ module ActionMCP
182
182
  end
183
183
 
184
184
  def set_protocol_version(version)
185
- version = ActionMCP::LATEST_VERSION if ActionMCP.configuration.vibed_ignore_version
186
185
  self.protocol_version = version
187
186
  save
188
187
  end
@@ -18,7 +18,14 @@ module ActionMCP
18
18
  prompt = prompt_class.new(params)
19
19
  prompt.with_context({ session: session })
20
20
 
21
- result = prompt.call
21
+ # Wrap prompt execution with Rails reloader for development
22
+ result = if Rails.env.development? && defined?(Rails.application.reloader)
23
+ Rails.application.reloader.wrap do
24
+ prompt.call
25
+ end
26
+ else
27
+ prompt.call
28
+ end
22
29
 
23
30
  if result.is_error
24
31
  send_jsonrpc_response(request_id, error: result)
@@ -50,12 +50,15 @@ module ActionMCP
50
50
  record = template.process(params[:uri])
51
51
  record.with_context({ session: session })
52
52
 
53
- if (resource = record.call)
54
- resource = [ resource ] unless resource.respond_to?(:each)
55
- content = resource.map(&:to_h)
56
- send_jsonrpc_response(id, result: { contents: content })
53
+ response = record.call
54
+
55
+ if response.error?
56
+ # Convert ResourceResponse errors to JSON-RPC errors
57
+ error_info = response.to_h
58
+ send_jsonrpc_error(id, error_info[:code], error_info[:message], error_info[:data])
57
59
  else
58
- send_jsonrpc_error(id, :invalid_params, "Resource not found")
60
+ # Handle successful response - ResourceResponse.contents is already an array
61
+ send_jsonrpc_response(id, result: { contents: response.contents.map(&:to_h) })
59
62
  end
60
63
  else
61
64
  send_jsonrpc_error(id, :invalid_params, "Invalid resource URI")
@@ -18,7 +18,9 @@ module ActionMCP
18
18
  end
19
19
 
20
20
  # Use session's registered tools instead of global registry
21
- tools = session.registered_tools.map do |tool_class|
21
+ registered_tools = session.registered_tools
22
+
23
+ tools = registered_tools.map do |tool_class|
22
24
  tool_class.to_h(protocol_version: protocol_version)
23
25
  end
24
26
 
@@ -52,7 +54,14 @@ module ActionMCP
52
54
  }
53
55
  })
54
56
 
55
- result = tool.call
57
+ # Wrap tool execution with Rails reloader for development
58
+ result = if Rails.env.development? && defined?(Rails.application.reloader)
59
+ Rails.application.reloader.wrap do
60
+ tool.call
61
+ end
62
+ else
63
+ tool.call
64
+ end
56
65
 
57
66
  if result.is_error
58
67
  # Convert ToolResponse error to proper JSON-RPC error format
@@ -30,9 +30,12 @@ module ActionMCP
30
30
 
31
31
  # Access the session store
32
32
  def session_store
33
- @session_store ||= SessionStoreFactory.create(
34
- ActionMCP.configuration.server_session_store_type
35
- )
33
+ current_type = ActionMCP.configuration.server_session_store_type
34
+ if @session_store.nil? || @session_store_type != current_type
35
+ @session_store_type = current_type
36
+ @session_store = SessionStoreFactory.create(current_type)
37
+ end
38
+ @session_store
36
39
  end
37
40
 
38
41
  # Available pubsub adapter types
@@ -43,6 +43,7 @@ module ActionMCP
43
43
  #
44
44
  # @return [String] The default tool name.
45
45
  def self.default_tool_name
46
+ return "" if name.nil?
46
47
  name.demodulize.underscore.sub(/_tool$/, "")
47
48
  end
48
49
 
@@ -53,6 +54,19 @@ module ActionMCP
53
54
  :tool
54
55
  end
55
56
 
57
+ def unregister_from_registry
58
+ ActionMCP::ToolsRegistry.unregister(self) if ActionMCP::ToolsRegistry.items.values.include?(self)
59
+ end
60
+
61
+ # Hook called when a class inherits from Tool
62
+ def inherited(subclass)
63
+ super
64
+ # Run the ActiveSupport load hook when a tool is defined
65
+ subclass.class_eval do
66
+ ActiveSupport.run_load_hooks(:action_mcp_tool, subclass)
67
+ end
68
+ end
69
+
56
70
  def annotate(key, value)
57
71
  self._annotations = _annotations.merge(key.to_s => value)
58
72
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.60.1"
5
+ VERSION = "0.70.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
@@ -124,4 +124,242 @@ namespace :action_mcp do
124
124
  task list: %i[list_tools list_prompts list_resources list_profiles] do
125
125
  # This task lists all tools, prompts, resources and profiles
126
126
  end
127
+
128
+ # bin/rails action_mcp:info
129
+ # bin/rails action_mcp:info[test]
130
+ desc "Display ActionMCP configuration for current or specified environment"
131
+ task :info, [ :env ] => :environment do |_t, args|
132
+ env = args[:env] || Rails.env
133
+
134
+ # Load configuration for the specified environment
135
+ original_env = Rails.env
136
+ Rails.env = env.to_s
137
+
138
+ # Reload configuration to get the environment-specific settings
139
+ config = ActionMCP::Configuration.new
140
+ config.load_profiles
141
+
142
+ puts "\e[35mActionMCP Configuration (#{env})\e[0m"
143
+ puts "\e[35m#{'=' * (25 + env.length)}\e[0m"
144
+
145
+ # Basic Information
146
+ puts "\n\e[36mBasic Information:\e[0m"
147
+ puts " Name: #{config.name}"
148
+ puts " Version: #{config.version}"
149
+ puts " Protocol Version: #{config.protocol_version}"
150
+ puts " Active Profile: #{config.active_profile}"
151
+
152
+ # Session Storage
153
+ puts "\n\e[36mSession Storage:\e[0m"
154
+ puts " Session Store Type: #{config.session_store_type}"
155
+ puts " Client Session Store: #{config.client_session_store_type || 'default'}"
156
+ puts " Server Session Store: #{config.server_session_store_type || 'default'}"
157
+
158
+ # Transport Configuration
159
+ puts "\n\e[36mTransport Configuration:\e[0m"
160
+ puts " SSE Heartbeat Interval: #{config.sse_heartbeat_interval}s"
161
+ puts " Post Response Preference: #{config.post_response_preference}"
162
+ puts " SSE Event Retention Period: #{config.sse_event_retention_period}"
163
+ puts " Max Stored SSE Events: #{config.max_stored_sse_events}"
164
+
165
+ # Pub/Sub Adapter
166
+ puts "\n\e[36mPub/Sub Adapter:\e[0m"
167
+ puts " Adapter: #{config.adapter || 'not configured'}"
168
+ if config.adapter
169
+ puts " Polling Interval: #{config.polling_interval}" if config.polling_interval
170
+ puts " Min Threads: #{config.min_threads}" if config.min_threads
171
+ puts " Max Threads: #{config.max_threads}" if config.max_threads
172
+ puts " Max Queue: #{config.max_queue}" if config.max_queue
173
+ end
174
+
175
+ # Authentication
176
+ puts "\n\e[36mAuthentication:\e[0m"
177
+ puts " Methods: #{config.authentication_methods.join(', ')}"
178
+ if config.oauth_config && config.oauth_config.any?
179
+ puts " OAuth Provider: #{config.oauth_config['provider']}"
180
+ puts " OAuth Scopes: #{config.oauth_config['scopes_supported']&.join(', ')}"
181
+ end
182
+
183
+ # Logging
184
+ puts "\n\e[36mLogging:\e[0m"
185
+ puts " Logging Enabled: #{config.logging_enabled}"
186
+ puts " Logging Level: #{config.logging_level}"
187
+
188
+ # Gateway
189
+ puts "\n\e[36mGateway:\e[0m"
190
+ puts " Gateway Class: #{config.gateway_class}"
191
+
192
+ # Capabilities
193
+ puts "\n\e[36mEnabled Capabilities:\e[0m"
194
+ capabilities = config.capabilities
195
+ if capabilities.any?
196
+ capabilities.each do |cap_name, cap_config|
197
+ puts " #{cap_name}: #{cap_config.inspect}"
198
+ end
199
+ else
200
+ puts " None"
201
+ end
202
+
203
+ # Available Profiles
204
+ puts "\n\e[36mAvailable Profiles:\e[0m"
205
+ config.profiles.each_key do |profile_name|
206
+ puts " - #{profile_name}"
207
+ end
208
+
209
+ # Restore original environment
210
+ Rails.env = original_env
211
+
212
+ puts "\n"
213
+ end
214
+
215
+ # bin/rails action_mcp:stats
216
+ desc "Display ActionMCP session and database statistics"
217
+ task stats: :environment do
218
+ puts "\e[35mActionMCP Statistics\e[0m"
219
+ puts "\e[35m===================\e[0m"
220
+
221
+ # Debug database connection
222
+ puts "\n\e[36mDatabase Debug:\e[0m"
223
+ puts " Rails Environment: #{Rails.env}"
224
+ puts " Rails Root: #{Rails.root}"
225
+ puts " Database Config: #{ActionMCP::ApplicationRecord.connection_db_config.configuration_hash.inspect}"
226
+ if ActionMCP::ApplicationRecord.connection.adapter_name.downcase.include?("sqlite")
227
+ db_path = ActionMCP::ApplicationRecord.connection_db_config.database
228
+ puts " Database Path: #{db_path}"
229
+ puts " Database Exists?: #{File.exist?(db_path)}" if db_path
230
+ end
231
+
232
+ # Session Statistics
233
+ puts "\n\e[36mSession Statistics:\e[0m"
234
+
235
+ begin
236
+ total_sessions = ActionMCP::Session.count
237
+ puts " Total Sessions: #{total_sessions}"
238
+
239
+ if total_sessions > 0
240
+ # Sessions by status
241
+ sessions_by_status = ActionMCP::Session.group(:status).count
242
+ puts " Sessions by Status:"
243
+ sessions_by_status.each do |status, count|
244
+ puts " #{status}: #{count}"
245
+ end
246
+
247
+ # Sessions by protocol version
248
+ sessions_by_protocol = ActionMCP::Session.group(:protocol_version).count
249
+ puts " Sessions by Protocol Version:"
250
+ sessions_by_protocol.each do |version, count|
251
+ puts " #{version}: #{count}"
252
+ end
253
+
254
+ # Active sessions (initialized and not ended)
255
+ active_sessions = ActionMCP::Session.where(status: "initialized", ended_at: nil).count
256
+ puts " Active Sessions: #{active_sessions}"
257
+
258
+ # Recent activity
259
+ recent_sessions = ActionMCP::Session.where("created_at > ?", 1.hour.ago).count
260
+ puts " Sessions Created (Last Hour): #{recent_sessions}"
261
+
262
+ # Session with most messages
263
+ if ActionMCP::Session.maximum(:messages_count)
264
+ busiest_session = ActionMCP::Session.order(messages_count: :desc).first
265
+ puts " Most Active Session: #{busiest_session.id} (#{busiest_session.messages_count} messages)"
266
+ end
267
+
268
+ # Average messages per session
269
+ avg_messages = ActionMCP::Session.average(:messages_count).to_f.round(2)
270
+ puts " Average Messages per Session: #{avg_messages}"
271
+ end
272
+ rescue StandardError => e
273
+ puts " Error accessing session data: #{e.message}"
274
+ puts " (Session store might be using volatile storage)"
275
+ end
276
+
277
+ # Message Statistics (if messages table exists)
278
+ puts "\n\e[36mMessage Statistics:\e[0m"
279
+
280
+ begin
281
+ if ActionMCP::ApplicationRecord.connection.table_exists?("action_mcp_session_messages")
282
+ total_messages = ActionMCP::Session::Message.count
283
+ puts " Total Messages: #{total_messages}"
284
+
285
+ if total_messages > 0
286
+ # Messages by direction
287
+ messages_by_direction = ActionMCP::Session::Message.group(:direction).count
288
+ puts " Messages by Direction:"
289
+ messages_by_direction.each do |direction, count|
290
+ puts " #{direction}: #{count}"
291
+ end
292
+
293
+ # Messages by type
294
+ messages_by_type = ActionMCP::Session::Message.group(:message_type).count.sort_by { |_type, count| -count }.first(10)
295
+ puts " Top Message Types:"
296
+ messages_by_type.each do |type, count|
297
+ puts " #{type}: #{count}"
298
+ end
299
+
300
+ # Recent messages
301
+ recent_messages = ActionMCP::Session::Message.where("created_at > ?", 1.hour.ago).count
302
+ puts " Messages (Last Hour): #{recent_messages}"
303
+ end
304
+ else
305
+ puts " Message table not found"
306
+ end
307
+ rescue StandardError => e
308
+ puts " Error accessing message data: #{e.message}"
309
+ end
310
+
311
+ # SSE Event Statistics (if table exists)
312
+ puts "\n\e[36mSSE Event Statistics:\e[0m"
313
+
314
+ begin
315
+ if ActionMCP::ApplicationRecord.connection.table_exists?("action_mcp_sse_events")
316
+ total_events = ActionMCP::Session::SSEEvent.count
317
+ puts " Total SSE Events: #{total_events}"
318
+
319
+ if total_events > 0
320
+ # Recent events
321
+ recent_events = ActionMCP::Session::SSEEvent.where("created_at > ?", 1.hour.ago).count
322
+ puts " SSE Events (Last Hour): #{recent_events}"
323
+
324
+ # Events by session
325
+ events_by_session = ActionMCP::Session::SSEEvent.joins(:session)
326
+ .group("action_mcp_sessions.id")
327
+ .count
328
+ .sort_by { |_session_id, count| -count }
329
+ .first(5)
330
+ puts " Top Sessions by SSE Events:"
331
+ events_by_session.each do |session_id, count|
332
+ puts " #{session_id}: #{count} events"
333
+ end
334
+ end
335
+ else
336
+ puts " SSE Events table not found"
337
+ end
338
+ rescue StandardError => e
339
+ puts " Error accessing SSE event data: #{e.message}"
340
+ end
341
+
342
+ # Storage Information
343
+ puts "\n\e[36mStorage Information:\e[0m"
344
+ puts " Session Store Type: #{ActionMCP.configuration.session_store_type}"
345
+ puts " Database Adapter: #{ActionMCP::ApplicationRecord.connection.adapter_name}"
346
+
347
+ # Database size (if SQLite)
348
+ begin
349
+ if ActionMCP::ApplicationRecord.connection.adapter_name.downcase.include?("sqlite")
350
+ db_config = Rails.application.config.database_configuration[Rails.env]
351
+ if db_config && db_config["database"]
352
+ db_file = db_config["database"]
353
+ if File.exist?(db_file)
354
+ size_mb = (File.size(db_file) / 1024.0 / 1024.0).round(2)
355
+ puts " Database Size: #{size_mb} MB"
356
+ end
357
+ end
358
+ end
359
+ rescue StandardError => e
360
+ puts " Could not determine database size: #{e.message}"
361
+ end
362
+
363
+ puts "\n"
364
+ end
127
365
  end
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.60.1
4
+ version: 0.70.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -284,6 +284,7 @@ files:
284
284
  - lib/action_mcp/renderable.rb
285
285
  - lib/action_mcp/resource.rb
286
286
  - lib/action_mcp/resource_callbacks.rb
287
+ - lib/action_mcp/resource_response.rb
287
288
  - lib/action_mcp/resource_template.rb
288
289
  - lib/action_mcp/resource_templates_registry.rb
289
290
  - lib/action_mcp/server.rb