actionmcp 0.14.0 → 0.16.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +152 -148
  3. data/Rakefile +1 -1
  4. data/app/controllers/action_mcp/{application_controller.rb → mcp_controller.rb} +3 -1
  5. data/app/controllers/action_mcp/messages_controller.rb +7 -5
  6. data/app/controllers/action_mcp/sse_controller.rb +19 -13
  7. data/app/models/action_mcp/session/message.rb +95 -90
  8. data/app/models/action_mcp/session/resource.rb +10 -6
  9. data/app/models/action_mcp/session/subscription.rb +9 -5
  10. data/app/models/action_mcp/session.rb +22 -13
  11. data/app/models/action_mcp.rb +2 -0
  12. data/config/routes.rb +2 -0
  13. data/db/migrate/20250308122801_create_action_mcp_sessions.rb +12 -10
  14. data/db/migrate/20250314230152_add_is_ping_to_session_message.rb +2 -0
  15. data/db/migrate/20250316005021_create_action_mcp_session_subscriptions.rb +3 -1
  16. data/db/migrate/20250316005649_create_action_mcp_session_resources.rb +4 -2
  17. data/exe/actionmcp_cli +57 -55
  18. data/lib/action_mcp/base_json_rpc_handler.rb +97 -0
  19. data/lib/action_mcp/callbacks.rb +122 -0
  20. data/lib/action_mcp/capability.rb +6 -3
  21. data/lib/action_mcp/client.rb +20 -26
  22. data/lib/action_mcp/client_json_rpc_handler.rb +69 -0
  23. data/lib/action_mcp/configuration.rb +8 -8
  24. data/lib/action_mcp/gem_version.rb +2 -0
  25. data/lib/action_mcp/instrumentation/controller_runtime.rb +38 -0
  26. data/lib/action_mcp/instrumentation/instrumentation.rb +26 -0
  27. data/lib/action_mcp/instrumentation/log_subscriber.rb +39 -0
  28. data/lib/action_mcp/instrumentation/resource_instrumentation.rb +40 -0
  29. data/lib/action_mcp/json_rpc/response.rb +18 -2
  30. data/lib/action_mcp/json_rpc_handler.rb +93 -21
  31. data/lib/action_mcp/log_subscriber.rb +28 -0
  32. data/lib/action_mcp/logging.rb +1 -3
  33. data/lib/action_mcp/prompt.rb +15 -6
  34. data/lib/action_mcp/prompt_response.rb +1 -1
  35. data/lib/action_mcp/prompts_registry.rb +1 -0
  36. data/lib/action_mcp/registry_base.rb +1 -0
  37. data/lib/action_mcp/resource_callbacks.rb +156 -0
  38. data/lib/action_mcp/resource_template.rb +18 -19
  39. data/lib/action_mcp/resource_templates_registry.rb +19 -25
  40. data/lib/action_mcp/sampling_request.rb +113 -0
  41. data/lib/action_mcp/server.rb +4 -1
  42. data/lib/action_mcp/server_json_rpc_handler.rb +90 -0
  43. data/lib/action_mcp/test_helper.rb +6 -2
  44. data/lib/action_mcp/tool.rb +12 -3
  45. data/lib/action_mcp/tool_response.rb +3 -2
  46. data/lib/action_mcp/transport/capabilities.rb +5 -1
  47. data/lib/action_mcp/transport/messaging.rb +2 -0
  48. data/lib/action_mcp/transport/prompts.rb +2 -0
  49. data/lib/action_mcp/transport/resources.rb +23 -6
  50. data/lib/action_mcp/transport/roots.rb +11 -0
  51. data/lib/action_mcp/transport/sampling.rb +14 -0
  52. data/lib/action_mcp/transport/sse_client.rb +11 -15
  53. data/lib/action_mcp/transport/stdio_client.rb +12 -14
  54. data/lib/action_mcp/transport/tools.rb +2 -0
  55. data/lib/action_mcp/transport/transport_base.rb +16 -15
  56. data/lib/action_mcp/transport.rb +2 -0
  57. data/lib/action_mcp/transport_handler.rb +3 -0
  58. data/lib/action_mcp/version.rb +1 -1
  59. data/lib/action_mcp.rb +8 -2
  60. data/lib/generators/action_mcp/install/install_generator.rb +4 -1
  61. data/lib/generators/action_mcp/install/templates/application_mcp_res_template.rb +2 -0
  62. data/lib/generators/action_mcp/resource_template/resource_template_generator.rb +2 -0
  63. data/lib/generators/action_mcp/resource_template/templates/resource_template.rb.erb +1 -1
  64. data/lib/tasks/action_mcp_tasks.rake +11 -6
  65. metadata +27 -14
@@ -17,13 +17,13 @@ module ActionMCP
17
17
  request = if line.is_a?(String)
18
18
  line.strip!
19
19
  return if line.empty?
20
+
20
21
  begin
21
22
  MultiJson.load(line)
22
23
  rescue MultiJson::ParseError => e
23
24
  Rails.logger.error("Failed to parse JSON: #{e.message}")
24
25
  return
25
26
  end
26
-
27
27
  else
28
28
  line
29
29
  end
@@ -46,22 +46,35 @@ module ActionMCP
46
46
  id = request["id"]
47
47
  params = request["params"]
48
48
 
49
+ # Common methods (both directions)
49
50
  case rpc_method
50
- when "initialize"
51
+ when "initialize" # [SERVER] Client initializing the connection
51
52
  transport.send_capabilities(id, params)
52
- when "ping"
53
+ when "ping" # [BOTH] Client ping
53
54
  transport.send_pong(id)
54
- when /^notifications\//
55
- puts "\e[31mProcessing notifications\e[0m"
56
- process_notifications(rpc_method, params)
57
- when /^prompts\//
55
+
56
+ # Methods that servers must implement (client → server)
57
+ when %r{^prompts/} # [SERVER] Prompt-related requests
58
58
  process_prompts(rpc_method, id, params)
59
- when /^resources\//
59
+ when %r{^resources/} # [SERVER] Resource-related requests
60
60
  process_resources(rpc_method, id, params)
61
- when /^tools\//
61
+ when %r{^tools/} # [SERVER] Tool-related requests
62
62
  process_tools(rpc_method, id, params)
63
- when "completion/complete"
63
+ when "completion/complete" # [SERVER] Completion requests
64
64
  process_completion_complete(id, params)
65
+
66
+ # Methods that clients must implement (server → client)
67
+ when "client/setLoggingLevel" # [CLIENT] Server configuring client logging
68
+ process_client_logging(id, params)
69
+ when %r{^roots/} # [CLIENT] Roots management
70
+ process_roots(rpc_method, id, params)
71
+ when %r{^sampling/} # [CLIENT] Sampling requests
72
+ process_sampling(rpc_method, id, params)
73
+
74
+ # Notifications (can go both ways)
75
+ when %r{^notifications/}
76
+ puts "\e[31mProcessing notifications\e[0m"
77
+ process_notifications(rpc_method, params)
65
78
  else
66
79
  puts "\e[31mUnknown method: #{rpc_method} #{request}\e[0m"
67
80
  end
@@ -70,17 +83,31 @@ module ActionMCP
70
83
  # @param rpc_method [String]
71
84
  def process_notifications(rpc_method, params)
72
85
  case rpc_method
73
- when "notifications/initialized"
86
+ when "notifications/initialized" # [SERVER] Client initialization complete
74
87
  puts "\e[31mInitialized\e[0m"
75
88
  transport.initialize!
76
- when "notifications/cancelled"
77
- puts "\e[31m Request #{params["requestId"]} cancelled: #{params["reason"]}\e[0m"
89
+ when "notifications/cancelled" # [BOTH] Request cancellation
90
+ puts "\e[31m Request #{params['requestId']} cancelled: #{params['reason']}\e[0m"
78
91
  # we don't need to do anything here
92
+ when "notifications/resources/updated" # [CLIENT] Resource update notification
93
+ puts "\e[31m Resource #{params['uri']} was updated\e[0m"
94
+ # Handle resource update notification
95
+ when "notifications/tools/list_changed" # [CLIENT] Tool list change notification
96
+ puts "\e[31m Tool list has changed\e[0m"
97
+ # Handle tool list change notification
98
+ when "notifications/prompts/list_changed" # [CLIENT] Prompt list change notification
99
+ puts "\e[31m Prompt list has changed\e[0m"
100
+ # Handle prompt list change notification
101
+ when "notifications/resources/list_changed" # [CLIENT] Resource list change notification
102
+ puts "\e[31m Resource list has changed\e[0m"
103
+ # Handle resource list change notification
79
104
  else
80
105
  Rails.logger.warn("Unknown notifications method: #{rpc_method}")
81
106
  end
82
107
  end
83
108
 
109
+ # Server methods (client → server)
110
+
84
111
  # @param id [String]
85
112
  # @param params [Hash]
86
113
  # @example {
@@ -117,9 +144,9 @@ module ActionMCP
117
144
  # @param params [Hash]
118
145
  def process_prompts(rpc_method, id, params)
119
146
  case rpc_method
120
- when "prompts/get"
147
+ when "prompts/get" # [SERVER] Get specific prompt
121
148
  transport.send_prompts_get(id, params["name"], params["arguments"])
122
- when "prompts/list"
149
+ when "prompts/list" # [SERVER] List available prompts
123
150
  transport.send_prompts_list(id)
124
151
  else
125
152
  Rails.logger.warn("Unknown prompts method: #{rpc_method}")
@@ -129,29 +156,74 @@ module ActionMCP
129
156
  # @param rpc_method [String]
130
157
  # @param id [String]
131
158
  # @param params [Hash]
132
- # Not implemented
133
159
  def process_resources(rpc_method, id, params)
134
160
  case rpc_method
135
- when "resources/list"
161
+ when "resources/list" # [SERVER] List available resources
136
162
  transport.send_resources_list(id)
137
- when "resources/templates/list"
163
+ when "resources/templates/list" # [SERVER] List resource templates
138
164
  transport.send_resource_templates_list(id)
139
- when "resources/read"
165
+ when "resources/read" # [SERVER] Read resource content
140
166
  transport.send_resource_read(id, params)
167
+ when "resources/subscribe" # [SERVER] Subscribe to resource updates
168
+ transport.send_resource_subscribe(id, params["uri"])
169
+ when "resources/unsubscribe" # [SERVER] Unsubscribe from resource updates
170
+ transport.send_resource_unsubscribe(id, params["uri"])
141
171
  else
142
172
  Rails.logger.warn("Unknown resources method: #{rpc_method}")
143
173
  end
144
174
  end
145
175
 
176
+ # @param rpc_method [String]
177
+ # @param id [String]
178
+ # @param params [Hash]
146
179
  def process_tools(rpc_method, id, params)
147
180
  case rpc_method
148
- when "tools/list"
181
+ when "tools/list" # [SERVER] List available tools
149
182
  transport.send_tools_list(id)
150
- when "tools/call"
183
+ when "tools/call" # [SERVER] Call a tool
151
184
  transport.send_tools_call(id, params&.dig("name"), params&.dig("arguments"))
152
185
  else
153
186
  Rails.logger.warn("Unknown tools method: #{rpc_method}")
154
187
  end
155
188
  end
189
+
190
+ # Client methods (server → client)
191
+
192
+ # @param id [String]
193
+ # @param params [Hash]
194
+ def process_client_logging(id, params)
195
+ level = params["level"]
196
+ transport.set_client_logging_level(id, level)
197
+ end
198
+
199
+ # @param rpc_method [String]
200
+ # @param id [String]
201
+ # @param params [Hash]
202
+ def process_roots(rpc_method, id, params)
203
+ case rpc_method
204
+ when "roots/list" # [CLIENT] List available roots
205
+ transport.send_roots_list(id)
206
+ when "roots/add" # [CLIENT] Add a root
207
+ transport.send_roots_add(id, params["uri"], params["name"])
208
+ when "roots/remove" # [CLIENT] Remove a root
209
+ transport.send_roots_remove(id, params["uri"])
210
+ else
211
+ Rails.logger.warn("Unknown roots method: #{rpc_method}")
212
+ end
213
+ end
214
+
215
+ # @param rpc_method [String]
216
+ # @param id [String]
217
+ # @param params [Hash]
218
+ def process_sampling(rpc_method, id, params)
219
+ case rpc_method
220
+ when "sampling/createMessage" # [CLIENT] Create a message using AI
221
+ # @param id [String]
222
+ # @param params [SamplingRequest]
223
+ transport.send_sampling_create_message(id, params)
224
+ else
225
+ Rails.logger.warn("Unknown sampling method: #{rpc_method}")
226
+ end
227
+ end
156
228
  end
157
229
  end
@@ -0,0 +1,28 @@
1
+ # In log_subscriber.rb
2
+ module ActionMCP
3
+ class LogSubscriber < ActiveSupport::LogSubscriber
4
+ def tool_call(event)
5
+ # Try both debug and info to ensure output regardless of logger level
6
+ debug "Tool: #{event.payload[:tool_name]} (#{event.duration.round(2)}ms)"
7
+ info "Tool: #{event.payload[:tool_name]} (#{event.duration.round(2)}ms)"
8
+
9
+ # Track total tool time for summary
10
+ Thread.current[:tool_runtime] ||= 0
11
+ Thread.current[:tool_runtime] += event.duration
12
+ end
13
+
14
+ def prompt_call(event)
15
+ # Add debug output to confirm method is called
16
+ puts "LogSubscriber#prompt_call called with: #{event.name}"
17
+
18
+ # Try both debug and info to ensure output regardless of logger level
19
+ debug "Prompt: #{event.payload[:prompt_name]} (#{event.duration.round(2)}ms)"
20
+ info "Prompt: #{event.payload[:prompt_name]} (#{event.duration.round(2)}ms)"
21
+
22
+ # Track total prompt time for summary
23
+ Thread.current[:prompt_runtime] ||= 0
24
+ Thread.current[:prompt_runtime] += event.duration
25
+ end
26
+ attach_to :action_mcp
27
+ end
28
+ end
@@ -11,9 +11,7 @@ module ActionMCP
11
11
 
12
12
  # Included hook to configure the logger.
13
13
  included do
14
- logger_instance = ActiveSupport::Logger.new(STDOUT)
15
- logger_instance.level = Logger.const_get(ActionMCP.configuration.logging_level.to_s.upcase)
16
- cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(logger_instance)
14
+ cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
17
15
  end
18
16
  end
19
17
  end
@@ -3,6 +3,7 @@
3
3
  module ActionMCP
4
4
  # Abstract base class for Prompts
5
5
  class Prompt < Capability
6
+ include ActionMCP::Callbacks
6
7
  class_attribute :_argument_definitions, instance_accessor: false, default: []
7
8
 
8
9
  # ---------------------------------------------------
@@ -29,6 +30,10 @@ module ActionMCP
29
30
 
30
31
  class << self
31
32
  alias default_capability_name default_prompt_name
33
+
34
+ def type
35
+ :prompt
36
+ end
32
37
  end
33
38
 
34
39
  # ---------------------------------------------------
@@ -56,9 +61,9 @@ module ActionMCP
56
61
  attribute arg_name, :string, default: default
57
62
  validates arg_name, presence: true if required
58
63
 
59
- if enum.present?
60
- validates arg_name, inclusion: { in: enum }, allow_blank: !required
61
- end
64
+ return unless enum.present?
65
+
66
+ validates arg_name, inclusion: { in: enum }, allow_blank: !required
62
67
  end
63
68
 
64
69
  # Returns the list of argument definitions.
@@ -108,8 +113,10 @@ module ActionMCP
108
113
  # Check validations before proceeding
109
114
  if valid?
110
115
  begin
111
- perform # Invoke the subclass-specific logic if valid
112
- rescue
116
+ run_callbacks :perform do
117
+ perform # Invoke the subclass-specific logic if valid
118
+ end
119
+ rescue StandardError
113
120
  # Handle exceptions during execution
114
121
  @response.mark_as_error!(:internal_error, message: "Unhandled Error executing prompt")
115
122
  end
@@ -132,7 +139,9 @@ module ActionMCP
132
139
 
133
140
  errors_info = errors.any? ? ", errors: #{errors.full_messages}" : ""
134
141
 
135
- "#<#{self.class.name} #{attributes_hash.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}, #{response_info}#{errors_info}>"
142
+ "#<#{self.class.name} #{attributes_hash.map do |k, v|
143
+ "#{k}: #{v.inspect}"
144
+ end.join(', ')}, #{response_info}#{errors_info}>"
136
145
  end
137
146
 
138
147
  # Override render to collect messages
@@ -45,7 +45,7 @@ module ActionMCP
45
45
  end
46
46
 
47
47
  # Alias as_json to to_h for consistency
48
- alias_method :as_json, :to_h
48
+ alias as_json to_h
49
49
 
50
50
  # Handle to_json directly
51
51
  def to_json(options = nil)
@@ -17,6 +17,7 @@ module ActionMCP
17
17
  def prompt_call(prompt_name, arguments)
18
18
  prompt = find(prompt_name)
19
19
  prompt = prompt.new(arguments)
20
+
20
21
  prompt.call
21
22
  end
22
23
 
@@ -13,6 +13,7 @@ module ActionMCP
13
13
  def items
14
14
  @items = item_klass.descendants.each_with_object({}) do |klass, hash|
15
15
  next if klass.abstract?
16
+
16
17
  hash[klass.capability_name] = klass
17
18
  end
18
19
  end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+ require "active_support/core_ext/module/attribute_accessors"
5
+
6
+ module ActionMCP
7
+ # = Action MCP Resource Template \Callbacks
8
+ #
9
+ # Action MCP Resource Template Callbacks provide hooks during the resource resolution lifecycle.
10
+ # These callbacks allow you to trigger logic during the resource resolution process.
11
+ # Available callbacks are:
12
+ #
13
+ # * <tt>before_resolve</tt>
14
+ # * <tt>around_resolve</tt>
15
+ # * <tt>after_resolve</tt>
16
+ module ResourceCallbacks
17
+ extend ActiveSupport::Concern
18
+ include ActiveSupport::Callbacks
19
+
20
+ class << self
21
+ include ActiveSupport::Callbacks
22
+ define_callbacks :execute
23
+ end
24
+
25
+ included do
26
+ define_callbacks :resolve, skip_after_callbacks_if_terminated: true
27
+ end
28
+
29
+ # These methods will be included into any Action MCP Resource Template object, adding
30
+ # callbacks for the +resolve+ method.
31
+ class_methods do
32
+ # Defines a callback that will get called right before the
33
+ # resource template's resolve method is executed.
34
+ #
35
+ # class OrdersTemplate < ApplicationMCPResTemplate
36
+ # description "Access order information"
37
+ # uri_template "ecommerce://customers/{customer_id}/orders/{order_id}"
38
+ # mime_type "application/json"
39
+ #
40
+ # parameter :customer_id,
41
+ # description: "Customer identifier",
42
+ # required: true
43
+ # parameter :order_id,
44
+ # description: "Order identifier",
45
+ # required: true
46
+ #
47
+ # before_resolve do |template|
48
+ # Rails.logger.info("Starting to resolve order: #{template.order_id} for customer: #{template.customer_id}")
49
+ # end
50
+ #
51
+ # def resolve
52
+ # order = MockOrder.find_by(id: order_id)
53
+ # return unless order
54
+ #
55
+ # ActionMCP::Resource.new(
56
+ # uri: "ecommerce://orders/#{order_id}",
57
+ # name: "Order #{order_id}",
58
+ # description: "Order information for order #{order_id}",
59
+ # mime_type: "application/json",
60
+ # size: order.to_json.length
61
+ # )
62
+ # end
63
+ # end
64
+ #
65
+ def before_resolve(*filters, &blk)
66
+ set_callback(:resolve, :before, *filters, &blk)
67
+ end
68
+
69
+ # Defines a callback that will get called right after the
70
+ # resource template's resolve method has finished.
71
+ #
72
+ # class OrdersTemplate < ApplicationMCPResTemplate
73
+ # description "Access order information"
74
+ # uri_template "ecommerce://customers/{customer_id}/orders/{order_id}"
75
+ # mime_type "application/json"
76
+ #
77
+ # parameter :customer_id,
78
+ # description: "Customer identifier",
79
+ # required: true
80
+ # parameter :order_id,
81
+ # description: "Order identifier",
82
+ # required: true
83
+ #
84
+ # after_resolve do |template|
85
+ # Rails.logger.info("Finished resolving order resource for order: #{template.order_id}")
86
+ # end
87
+ #
88
+ # def resolve
89
+ # order = MockOrder.find_by(id: order_id)
90
+ # return unless order
91
+ #
92
+ # ActionMCP::Resource.new(
93
+ # uri: "ecommerce://orders/#{order_id}",
94
+ # name: "Order #{order_id}",
95
+ # description: "Order information for order #{order_id}",
96
+ # mime_type: "application/json",
97
+ # size: order.to_json.length
98
+ # )
99
+ # end
100
+ # end
101
+ #
102
+ def after_resolve(*filters, &blk)
103
+ set_callback(:resolve, :after, *filters, &blk)
104
+ end
105
+
106
+ # Defines a callback that will get called around the resource template's resolve method.
107
+ #
108
+ # class OrdersTemplate < ApplicationMCPResTemplate
109
+ # description "Access order information"
110
+ # uri_template "ecommerce://customers/{customer_id}/orders/{order_id}"
111
+ # mime_type "application/json"
112
+ #
113
+ # parameter :customer_id,
114
+ # description: "Customer identifier",
115
+ # required: true
116
+ # parameter :order_id,
117
+ # description: "Order identifier",
118
+ # required: true
119
+ #
120
+ # around_resolve do |template, block|
121
+ # start_time = Time.current
122
+ # Rails.logger.info("Starting resolution for order: #{template.order_id}")
123
+ #
124
+ # resource = block.call
125
+ #
126
+ # if resource
127
+ # Rails.logger.info("Order #{template.order_id} resolved successfully in #{Time.current - start_time}s")
128
+ # else
129
+ # Rails.logger.info("Order #{template.order_id} not found")
130
+ # end
131
+ #
132
+ # resource
133
+ # end
134
+ #
135
+ # def resolve
136
+ # order = MockOrder.find_by(id: order_id)
137
+ # return unless order
138
+ #
139
+ # ActionMCP::Resource.new(
140
+ # uri: "ecommerce://orders/#{order_id}",
141
+ # name: "Order #{order_id}",
142
+ # description: "Order information for order #{order_id}",
143
+ # mime_type: "application/json",
144
+ # size: order.to_json.length
145
+ # )
146
+ # end
147
+ # end
148
+ #
149
+ # You can access the return value of the resolve method as shown above.
150
+ #
151
+ def around_resolve(*filters, &blk)
152
+ set_callback(:resolve, :around, *filters, &blk)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -7,12 +7,14 @@ module ActionMCP
7
7
  # Add ActiveModel capabilities
8
8
  include ActiveModel::Model
9
9
  include ActiveModel::Validations
10
+ include ResourceCallbacks
11
+ include Logging
10
12
 
11
13
  # Track all registered templates
12
14
  @registered_templates = []
13
15
 
14
16
  class << self
15
- attr_reader :registered_templates
17
+ attr_reader :registered_templates, :description, :uri_template, :mime_type, :template_name, :parameters
16
18
 
17
19
  def abstract?
18
20
  @abstract ||= false
@@ -29,8 +31,6 @@ module ActionMCP
29
31
  subclass.instance_variable_set(:@required_parameters, [])
30
32
  end
31
33
 
32
- attr_reader :description, :uri_template, :mime_type, :template_name, :parameters
33
-
34
34
  # Track required parameters for validation
35
35
  def required_parameters
36
36
  @required_parameters ||= []
@@ -48,7 +48,7 @@ module ActionMCP
48
48
  end
49
49
 
50
50
  # Alias parameter to attribute for clarity
51
- alias_method :attribute, :parameter
51
+ alias attribute parameter
52
52
 
53
53
  def parameters
54
54
  @parameters || {}
@@ -87,6 +87,7 @@ module ActionMCP
87
87
  mimeType: @mime_type
88
88
  }.compact
89
89
  end
90
+
90
91
  def capability_name
91
92
  @capability_name ||= name.demodulize.underscore.sub(/_template$/, "")
92
93
  end
@@ -134,7 +135,7 @@ module ActionMCP
134
135
 
135
136
  # Add any remaining text after the last parameter
136
137
  if current_pos < @uri_template.length
137
- suffix = Regexp.escape(@uri_template[current_pos..-1])
138
+ suffix = Regexp.escape(@uri_template[current_pos..])
138
139
  regex_parts << suffix
139
140
  end
140
141
 
@@ -159,9 +160,9 @@ module ActionMCP
159
160
  def parse_uri_template(template)
160
161
  # Parse the URI template to get schema and pattern
161
162
  # Format: schema://path/{param1}/{param2}...
162
- if template =~ /^([^:]+):\/\/(.+)$/
163
- schema = $1
164
- pattern = $2
163
+ if template =~ %r{^([^:]+)://(.+)$}
164
+ schema = ::Regexp.last_match(1)
165
+ pattern = ::Regexp.last_match(2)
165
166
 
166
167
  # Replace parameter placeholders with generic markers to compare structure
167
168
  normalized_pattern = pattern.gsub(/\{[^}]+\}/, "{param}")
@@ -180,17 +181,17 @@ module ActionMCP
180
181
  next if registered_class == self || registered_class.abstract?
181
182
  next unless registered_class.uri_template
182
183
  # Ignore conflicts with resource templates that have the same name
183
- next if registered_class.name == self.name
184
+ next if registered_class.name == name
184
185
 
185
186
  existing_template_data = parse_uri_template(registered_class.uri_template)
186
187
 
187
188
  # Check if schema and structure are the same
188
- if new_template_data[:schema] == existing_template_data[:schema] &&
189
- are_potentially_ambiguous?(new_template_data[:pattern], existing_template_data[:pattern])
189
+ next unless new_template_data[:schema] == existing_template_data[:schema] &&
190
+ are_potentially_ambiguous?(new_template_data[:pattern], existing_template_data[:pattern])
190
191
 
191
- # Use a consistent error message format for all conflicts
192
- raise ArgumentError, "URI template conflict detected: '#{new_template}' conflicts with existing template '#{registered_class.uri_template}' registered by #{registered_class.name}"
193
- end
192
+ # Use a consistent error message format for all conflicts
193
+ raise ArgumentError,
194
+ "URI template conflict detected: '#{new_template}' conflicts with existing template '#{registered_class.uri_template}' registered by #{registered_class.name}"
194
195
  end
195
196
  end
196
197
 
@@ -216,7 +217,7 @@ module ActionMCP
216
217
  # If we have the same number of segments and same number of parameters,
217
218
  # but the patterns aren't identical, they could be ambiguous
218
219
  # due to parameter position swapping
219
- if param_segments1 > 0 && param_segments1 == param_segments2
220
+ if param_segments1.positive? && param_segments1 == param_segments2
220
221
  # Create pattern maps (P for param, S for static)
221
222
  pattern_map1 = segments1.map { |s| s.include?("{param}") ? "P" : "S" }
222
223
  pattern_map2 = segments2.map { |s| s.include?("{param}") ? "P" : "S" }
@@ -241,11 +242,9 @@ module ActionMCP
241
242
  end
242
243
 
243
244
  # Add custom validation for required parameters
244
- validate do |template|
245
+ validate do |_template|
245
246
  self.class.required_parameters.each do |param|
246
- if self.send(param).nil? || self.send(param).to_s.empty?
247
- errors.add(param, "can't be blank")
248
- end
247
+ errors.add(param, "can't be blank") if send(param).nil? || send(param).to_s.empty?
249
248
  end
250
249
  end
251
250
 
@@ -73,8 +73,6 @@ module ActionMCP
73
73
  end
74
74
  elsif matching_templates.size == 1
75
75
  matching_templates.first
76
- else
77
- nil
78
76
  end
79
77
  end
80
78
 
@@ -117,11 +115,11 @@ module ActionMCP
117
115
  # Extract parameters
118
116
  params = {}
119
117
  template_segments.each_with_index do |template_segment, index|
120
- if template_segment.start_with?("{") && template_segment.end_with?("}")
121
- # Extract parameter name without braces
122
- param_name = template_segment[1...-1].to_sym
123
- params[param_name] = uri_segments[index]
124
- end
118
+ next unless template_segment.start_with?("{") && template_segment.end_with?("}")
119
+
120
+ # Extract parameter name without braces
121
+ param_name = template_segment[1...-1].to_sym
122
+ params[param_name] = uri_segments[index]
125
123
  end
126
124
 
127
125
  params
@@ -131,28 +129,24 @@ module ActionMCP
131
129
 
132
130
  # Parse a concrete URI
133
131
  def parse_uri(uri)
134
- if uri =~ /^([^:]+):\/\/(.+)$/
135
- {
136
- schema: $1,
137
- path: $2,
138
- original: uri
139
- }
140
- else
141
- nil
142
- end
132
+ return unless uri =~ %r{^([^:]+)://(.+)$}
133
+
134
+ {
135
+ schema: ::Regexp.last_match(1),
136
+ path: ::Regexp.last_match(2),
137
+ original: uri
138
+ }
143
139
  end
144
140
 
145
141
  # Parse a URI template
146
142
  def parse_uri_template(template)
147
- if template =~ /^([^:]+):\/\/(.+)$/
148
- {
149
- schema: $1,
150
- path: $2,
151
- original: template
152
- }
153
- else
154
- nil
155
- end
143
+ return unless template =~ %r{^([^:]+)://(.+)$}
144
+
145
+ {
146
+ schema: ::Regexp.last_match(1),
147
+ path: ::Regexp.last_match(2),
148
+ original: template
149
+ }
156
150
  end
157
151
  end
158
152
  end