actionmcp 0.82.0 → 0.83.1

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: b0ca96b3275c9dcece973bd75cc7eb4585fea06f6a99d8dec6a7603817c28b69
4
- data.tar.gz: a188765f7176684ee0875a9eecf25cf07ff4c2338c93be7fc5fe29954f26bbb0
3
+ metadata.gz: 34a7a67e4f2e58c82a8a06bb11adf328747ed61a38e4c06598b21657556b8779
4
+ data.tar.gz: 0a81e9eaec25dc3906a9476fff4c9458da889fd201cb644b46ba1074ea46d5ad
5
5
  SHA512:
6
- metadata.gz: 678b6231e5c2df59dc9b90c26d5135cfb2ceb2aaa45ed55e774dc9700cf15f634d8a796fa2c9d3e2a78896bf21f41a86d303f2ca97db4445d289f8a24a8892b5
7
- data.tar.gz: 2d1458519c3b69045a8f885be8da811544de78e793cc191ca2145d08ebc41a019a209b3f51fc9bcc9138e72b75f6b1fcc11428e01a06b1b6326f7bcc902d7dee
6
+ metadata.gz: 76e455eefa6f0fb9c055d4a799ba38f09993c7562746733c05e15e8bc256df112e24788989dce2dbe0bfa290b77eedc54f89a480422a6805050bde18fbb51d2f
7
+ data.tar.gz: 8c2fb33e02738de33451097b92c743acf053bdd759be14380fd62a098153b59a925aa8b7f490908457835d9cdb5de903a07e6d1bfe0b741aebe6f28ce64185b5
@@ -4,6 +4,11 @@ module ActionMCP
4
4
  class UnauthorizedError < StandardError; end
5
5
 
6
6
  class Gateway
7
+ # Whitelist of allowed identity attribute names to prevent method shadowing
8
+ # and unauthorized attribute assignment. Extend this list if you use custom
9
+ # identifier names in your GatewayIdentifier implementations.
10
+ ALLOWED_IDENTITY_KEYS = %w[user api_key jwt bearer token account session].freeze
11
+
7
12
  class << self
8
13
  # pluck in one or many GatewayIdentifier classes
9
14
  def identified_by(*klasses)
@@ -69,13 +74,21 @@ module ActionMCP
69
74
 
70
75
  def assign_identities(identities)
71
76
  identities.each do |name, value|
77
+ name_str = name.to_s
78
+
79
+ # Validate identity key against whitelist to prevent method shadowing
80
+ unless ALLOWED_IDENTITY_KEYS.include?(name_str)
81
+ raise ArgumentError, "Invalid identity key: '#{name_str}'. " \
82
+ "Allowed keys: #{ALLOWED_IDENTITY_KEYS.join(', ')}"
83
+ end
84
+
72
85
  # define accessor on the fly
73
86
  self.class.attr_reader name unless respond_to?(name)
74
- instance_variable_set("@#{name}", value)
87
+ instance_variable_set("@#{name_str}", value)
75
88
 
76
89
  # also set current context if you have one
77
- ActionMCP::Current.public_send("#{name}=", value) if
78
- ActionMCP::Current.respond_to?("#{name}=")
90
+ ActionMCP::Current.public_send("#{name_str}=", value) if
91
+ ActionMCP::Current.respond_to?("#{name_str}=")
79
92
  end
80
93
  ActionMCP::Current.gateway = self if
81
94
  ActionMCP::Current.respond_to?(:gateway=)
@@ -48,6 +48,11 @@ module ActionMCP
48
48
  @required_parameters ||= []
49
49
  end
50
50
 
51
+ # Cache compiled regex patterns for URI matching to avoid recompilation
52
+ def uri_regex_cache
53
+ @uri_regex_cache ||= {}
54
+ end
55
+
51
56
  def parameter(name, description:, required: false, **options)
52
57
  @parameters ||= {}
53
58
  @parameters[name] = { description: description, required: required, **options }
@@ -139,48 +144,55 @@ module ActionMCP
139
144
 
140
145
  # Extract parameters from a URI using the template pattern
141
146
  def extract_params_from_uri(uri_string)
142
- # Convert template parameters to named capture groups
143
- regex_parts = []
144
- current_pos = 0
145
- param_names = []
146
-
147
- # Find all template parameters like {param_name}
148
- @uri_template.scan(/\{([^}]+)\}/) do |param_name|
149
- param_names << param_name[0]
150
-
151
- # Get the position of this parameter in the template
152
- param_start = @uri_template.index("{#{param_name[0]}}", current_pos)
153
-
154
- # Add the text before the parameter (escaped)
155
- if param_start > current_pos
156
- prefix = Regexp.escape(@uri_template[current_pos...param_start])
157
- regex_parts << prefix
158
- end
147
+ # Check cache for compiled regex and param names
148
+ cache_entry = uri_regex_cache[@uri_template]
159
149
 
160
- # Add the named capture group
161
- regex_parts << "(?<#{param_name[0]}>[^/]+)"
150
+ unless cache_entry
151
+ # Convert template parameters to named capture groups
152
+ regex_parts = []
153
+ current_pos = 0
154
+ param_names = []
162
155
 
163
- # Update current position
164
- current_pos = param_start + param_name[0].length + 2 # +2 for { and }
165
- end
156
+ # Find all template parameters like {param_name}
157
+ @uri_template.scan(/\{([^}]+)\}/) do |param_name|
158
+ param_names << param_name[0]
166
159
 
167
- # Add any remaining text after the last parameter
168
- if current_pos < @uri_template.length
169
- suffix = Regexp.escape(@uri_template[current_pos..])
170
- regex_parts << suffix
171
- end
160
+ # Get the position of this parameter in the template
161
+ param_start = @uri_template.index("{#{param_name[0]}}", current_pos)
162
+
163
+ # Add the text before the parameter (escaped)
164
+ if param_start > current_pos
165
+ prefix = Regexp.escape(@uri_template[current_pos...param_start])
166
+ regex_parts << prefix
167
+ end
172
168
 
173
- # Build the final regex
174
- regex_pattern = regex_parts.join
175
- regex = Regexp.new("^#{regex_pattern}$")
169
+ # Add the named capture group
170
+ regex_parts << "(?<#{param_name[0]}>[^/]+)"
171
+
172
+ # Update current position
173
+ current_pos = param_start + param_name[0].length + 2 # +2 for { and }
174
+ end
175
+
176
+ # Add any remaining text after the last parameter
177
+ if current_pos < @uri_template.length
178
+ suffix = Regexp.escape(@uri_template[current_pos..])
179
+ regex_parts << suffix
180
+ end
181
+
182
+ # Build the final regex and cache it
183
+ regex_pattern = regex_parts.join
184
+ regex = Regexp.new("^#{regex_pattern}$")
185
+ cache_entry = { regex: regex, param_names: param_names }
186
+ uri_regex_cache[@uri_template] = cache_entry
187
+ end
176
188
 
177
- # Try to match the URI
178
- match_data = regex.match(uri_string)
189
+ # Try to match the URI using cached regex
190
+ match_data = cache_entry[:regex].match(uri_string)
179
191
  return nil unless match_data
180
192
 
181
193
  # Extract named captures as parameters
182
194
  params = {}
183
- param_names.each do |name|
195
+ cache_entry[:param_names].each do |name|
184
196
  params[name.to_sym] = match_data[name] if match_data[name]
185
197
  end
186
198
 
@@ -240,9 +240,9 @@ module ActionMCP
240
240
  prop_definition.merge!(opts) if opts.any?
241
241
 
242
242
  self._schema_properties = _schema_properties.merge(prop_name.to_s => prop_definition)
243
- self._required_properties = _required_properties.dup.tap do |req|
244
- req << prop_name.to_s if required
245
- end
243
+ new_required = _required_properties.dup
244
+ new_required << prop_name.to_s if required
245
+ self._required_properties = new_required
246
246
 
247
247
  # Map the JSON Schema type to an ActiveModel attribute type.
248
248
  attribute prop_name, map_json_type_to_active_model_type(type), default: default
@@ -271,9 +271,9 @@ module ActionMCP
271
271
  collection_definition[:description] = description if description && !description.empty?
272
272
 
273
273
  self._schema_properties = _schema_properties.merge(prop_name.to_s => collection_definition)
274
- self._required_properties = _required_properties.dup.tap do |req|
275
- req << prop_name.to_s if required
276
- end
274
+ new_required = _required_properties.dup
275
+ new_required << prop_name.to_s if required
276
+ self._required_properties = new_required
277
277
 
278
278
  # Map the type - for number arrays, use our custom type instance
279
279
  mapped_type = if type == "number"
@@ -26,8 +26,13 @@ module ActionMCP
26
26
  # Handle parameter validation errors
27
27
  error_response(:invalid_params, message: e.message)
28
28
  rescue StandardError => e
29
- # FIXME, we should maybe not return the error message to the user
30
- error_response(:invalid_params, message: "Tool execution failed: #{e.message}")
29
+ # Hide error details in production to prevent information disclosure
30
+ message = if Rails.env.production?
31
+ "Tool execution failed"
32
+ else
33
+ "Tool execution failed: #{e.message}"
34
+ end
35
+ error_response(:invalid_params, message: message)
31
36
  end
32
37
 
33
38
  def item_klass
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.82.0"
5
+ VERSION = "0.83.1"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
@@ -49,6 +49,28 @@ namespace :action_mcp do
49
49
  puts "\n"
50
50
  end
51
51
 
52
+ # bin/rails action_mcp:list_profiles
53
+ desc "List all available profiles"
54
+ task list_profiles: :environment do
55
+ # Ensure Rails eager loads all classes
56
+ Rails.application.eager_load!
57
+
58
+ puts "\e[35mACTION MCP PROFILES\e[0m" # Purple
59
+ puts "\e[35m-------------------\e[0m" # Purple
60
+
61
+ profiles = ActionMCP.configuration.profiles
62
+
63
+ if profiles.any?
64
+ profiles.each_key do |profile_name|
65
+ puts "\e[35m#{profile_name}\e[0m"
66
+ end
67
+ else
68
+ puts " No profiles configured"
69
+ end
70
+
71
+ puts "\n"
72
+ end
73
+
52
74
  # bin/rails action_mcp:show_profile[profile_name]
53
75
  desc "Show configuration for a specific profile"
54
76
  task :show_profile, [ :profile_name ] => :environment do |_t, args|
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.82.0
4
+ version: 0.83.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 8.0.1
18
+ version: 8.0.4
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 8.0.1
25
+ version: 8.0.4
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: concurrent-ruby
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 8.0.1
74
+ version: 8.0.4
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
- version: 8.0.1
81
+ version: 8.0.4
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: zeitwerk
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -93,20 +93,6 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '2.6'
96
- - !ruby/object:Gem::Dependency
97
- name: ostruct
98
- requirement: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - ">="
101
- - !ruby/object:Gem::Version
102
- version: '0'
103
- type: :runtime
104
- prerelease: false
105
- version_requirements: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - ">="
108
- - !ruby/object:Gem::Version
109
- version: '0'
110
96
  - !ruby/object:Gem::Dependency
111
97
  name: json_schemer
112
98
  requirement: !ruby/object:Gem::Requirement