activeadmin_mcp 0.0.4 → 0.1.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.
@@ -1,29 +1,34 @@
1
- # frozen_string_literal: true
2
-
3
1
  module ActiveadminMcp
4
- # Records the field names declared by an ActiveAdmin `form do ... end` block.
2
+ # Reads an ActiveAdmin `form do ... end` block and reports what each input
3
+ # tells a client about how to fill the field in.
5
4
  #
6
5
  # ActiveAdmin form blocks are arbitrary Formtastic DSL — `input`, `inputs`,
7
- # `actions`, helper calls, conditionals — so the block is run against this
8
- # stand-in form builder. Every `input :field` records `:field`; every other
9
- # message (including unknown helpers) is swallowed and returns self, so the
10
- # block executes without a real view context. Nested `has_many` associations
11
- # are intentionally not descended into: the updater only writes flat
12
- # attributes, and descending would record association fields as top-level.
6
+ # `actions`, application helpers, conditionals — so the block is run against
7
+ # this stand-in form builder rather than parsed. Every `input :field` is
8
+ # recorded; every other message (including helpers that would need a view
9
+ # context we do not have) is swallowed and returns self, so the block runs to
10
+ # completion outside a request.
11
+ #
12
+ # Only the options that describe the field to whoever is filling it in are
13
+ # kept. Presentation options (`input_html:`, `wrapper_html:` and the rest)
14
+ # say nothing an MCP client can act on and are dropped.
13
15
  class FormFieldCollector
14
- attr_reader :fields
16
+ DESCRIBED_TEXT_OPTIONS = %i[label hint].freeze
15
17
 
16
18
  def initialize
17
- @fields = []
19
+ @inputs = []
18
20
  end
19
21
 
20
22
  def collect(&block)
21
23
  instance_exec(self, &block)
22
- @fields.uniq
24
+ @inputs
23
25
  end
24
26
 
25
- def input(name, *_args, **_opts, &_block)
26
- @fields << name.to_sym if name.respond_to?(:to_sym)
27
+ def input(name, *_args, **options, &_block)
28
+ return self unless name.respond_to?(:to_sym)
29
+ return self if declared?(name.to_sym)
30
+
31
+ @inputs << describe(name.to_sym, options)
27
32
  self
28
33
  end
29
34
 
@@ -32,7 +37,14 @@ module ActiveadminMcp
32
37
  self
33
38
  end
34
39
 
35
- def has_many(*_args, **_opts)
40
+ # Recorded as a group of its own rather than descended into, so a client
41
+ # can tell an association's fields from the record's own. Flattening them
42
+ # would advertise `body` as an attribute of the parent record.
43
+ def has_many(name, *_args, **_opts, &block)
44
+ return self unless name.respond_to?(:to_sym)
45
+ return self if declared?(name.to_sym)
46
+
47
+ @inputs << { name: name.to_sym, nested: block ? self.class.new.collect(&block) : [] }
36
48
  self
37
49
  end
38
50
 
@@ -44,5 +56,46 @@ module ActiveadminMcp
44
56
  def respond_to_missing?(_name, _include_private = false)
45
57
  true
46
58
  end
59
+
60
+ private
61
+
62
+ def declared?(name)
63
+ @inputs.any? { |input| input[:name] == name }
64
+ end
65
+
66
+ def describe(name, options)
67
+ described = { name: name }
68
+
69
+ described[:as] = options[:as].to_sym if scalar_name?(options[:as])
70
+ described[:required] = options[:required] if [true, false].include?(options[:required])
71
+
72
+ DESCRIBED_TEXT_OPTIONS.each do |key|
73
+ described[key] = options[key].to_s if scalar_name?(options[key])
74
+ end
75
+
76
+ values = allowed_values(options[:collection])
77
+ described[:collection] = values if values
78
+
79
+ described
80
+ end
81
+
82
+ # A helper the collector swallowed comes back as the collector itself, so
83
+ # anything that is not plain text is not something to report as a label.
84
+ def scalar_name?(value)
85
+ value.is_a?(String) || value.is_a?(Symbol)
86
+ end
87
+
88
+ # Only a literal array is resolved. A relation would mean firing a query
89
+ # from what is meant to be a description, and can be arbitrarily large; a
90
+ # proc usually needs the view context this collector does not have. Either
91
+ # is omitted rather than evaluated.
92
+ def allowed_values(collection)
93
+ return nil unless collection.is_a?(Array)
94
+
95
+ values = collection.map { |entry| entry.is_a?(Array) ? entry.last : entry }
96
+ return nil unless values.all? { |value| scalar_name?(value) || value.is_a?(Numeric) }
97
+
98
+ values
99
+ end
47
100
  end
48
101
  end
@@ -0,0 +1,161 @@
1
+ module ActiveadminMcp
2
+ # Creates and updates ActiveAdmin-managed records by dispatching the
3
+ # resource's own create and update actions, so a write from an MCP client
4
+ # goes through the same permitted params, callbacks, before_action chain and
5
+ # authorization as the same write made by clicking Save in the admin UI.
6
+ #
7
+ # Three gates run before anything is dispatched: the resource must expose the
8
+ # action, the authorization adapter must allow it, and the resource must
9
+ # declare what may be written. The controller then applies all three again,
10
+ # against the record it builds or loads.
11
+ class RecordWriter
12
+ CREATE = :create
13
+ UPDATE = :update
14
+
15
+ def initialize(resource:, current_user:)
16
+ @resource = resource
17
+ @current_user = current_user
18
+ @config = resource[:config]
19
+ end
20
+
21
+ def create(attributes:)
22
+ return error("Resource is not creatable: #{resource_name}") unless exposes?(CREATE)
23
+
24
+ permitted = resolve_permitted(attributes)
25
+ refusal = permit_refusal(permitted)
26
+ return refusal if refusal
27
+ return error("Not authorized to create #{resource_name}") unless authorized?(CREATE, @config.resource_class)
28
+
29
+ write(
30
+ action: CREATE,
31
+ verb: :post,
32
+ path: @config.route_collection_path,
33
+ attributes: attributes,
34
+ written: permitted.keys,
35
+ written_key: :created,
36
+ description: "Create #{resource_name}"
37
+ )
38
+ end
39
+
40
+ def update(id:, attributes:)
41
+ return error("Resource is not editable: #{resource_name}") unless exposes?(UPDATE)
42
+
43
+ record = @resource[:model].find_by(id: id)
44
+ return error("Record not found: #{resource_name}##{id}") unless record
45
+
46
+ permitted = resolve_permitted(attributes)
47
+ refusal = permit_refusal(permitted)
48
+ return refusal if refusal
49
+ return error("Not authorized to update #{resource_name}##{id}") unless authorized?(UPDATE, record)
50
+
51
+ write(
52
+ action: UPDATE,
53
+ verb: :patch,
54
+ path: @config.route_instance_path(record),
55
+ attributes: attributes,
56
+ written: permitted.keys,
57
+ written_key: :updated,
58
+ path_params: { id: record.to_param },
59
+ description: "Update #{resource_name}##{id}"
60
+ )
61
+ end
62
+
63
+ private
64
+
65
+ def write(action:, verb:, path:, attributes:, written:, written_key:, description:, path_params: {})
66
+ record = nil
67
+
68
+ outcome = ControllerDispatcher.new(config: @config, current_user: @current_user).call(
69
+ action: action,
70
+ verb: verb,
71
+ path: path,
72
+ params: { @config.param_key => attributes },
73
+ path_params: path_params
74
+ ) { |controller| record = controller.send(:get_resource_ivar) }
75
+
76
+ return rejection(record, description, outcome) unless saved?(record)
77
+
78
+ {
79
+ resource: resource_name,
80
+ id: record.id,
81
+ written_key => written,
82
+ record: without_sensitive_attributes(record),
83
+ }
84
+ end
85
+
86
+ def saved?(record)
87
+ !record.nil? && record.persisted? && record.errors.empty?
88
+ end
89
+
90
+ # The controller declined to save. Validation messages are the usual
91
+ # reason, and they are read off the record rather than the response
92
+ # because a rejected write re-renders the admin form — a render the
93
+ # synthesized request may well not survive, leaving the dispatch itself
94
+ # reported as a failure even though the record knows exactly what was
95
+ # wrong with it.
96
+ #
97
+ # With no record and no validation messages, anything the controller put
98
+ # in the flash is the best account left of what the admin UI would have
99
+ # told the user.
100
+ def rejection(record, description, outcome)
101
+ messages = record ? record.errors.full_messages : []
102
+ return error("Validation failed", details: messages) if messages.any?
103
+ return outcome if outcome[:error]
104
+
105
+ error("#{description} failed", details: Array(outcome[:flash]&.values).presence)
106
+ end
107
+
108
+ def exposes?(action)
109
+ @config.defined_actions.include?(action)
110
+ end
111
+
112
+ def authorized?(action, subject)
113
+ Authorization.for(@config, @current_user).authorized?(action, subject)
114
+ end
115
+
116
+ # Turns what the controller says it would permit into a refusal, or nil to
117
+ # go ahead. The answer is not used to filter the write — dispatch means
118
+ # ActiveAdmin applies permit_params itself — but to refuse early, and with
119
+ # a reason, in the two cases where dispatching would otherwise fail
120
+ # obscurely: a resource that never declared permit_params, and a call whose
121
+ # every attribute would be dropped.
122
+ #
123
+ # A resource with no permit_params cannot be written through ActiveAdmin's
124
+ # own forms either — Rails raises ForbiddenAttributesError on the
125
+ # unpermitted params — so refusing it says what the admin UI would.
126
+ def permit_refusal(permitted)
127
+ if permitted.nil?
128
+ return error("Resource declares no permit_params, so nothing may be written: #{resource_name}")
129
+ end
130
+
131
+ error("No permitted attributes to write") if permitted.empty?
132
+ end
133
+
134
+ # Asks the resource's own controller what it would permit, or nil when it
135
+ # has nothing to say because permit_params was never declared.
136
+ def resolve_permitted(attributes)
137
+ param_key = @config.param_key.to_sym
138
+ controller = @config.controller.new
139
+ controller.params = ActionController::Parameters.new(param_key => attributes)
140
+ permitted = controller.send(:permitted_params)
141
+ scoped = permitted && permitted[param_key]
142
+ scoped&.to_h&.symbolize_keys
143
+ rescue StandardError
144
+ nil
145
+ end
146
+
147
+ def without_sensitive_attributes(record)
148
+ record.as_json.except(*ResourceRegistry.sensitive_attributes)
149
+ end
150
+
151
+ def resource_name
152
+ @resource[:name]
153
+ end
154
+
155
+ def error(message, details: nil)
156
+ result = { error: message }
157
+ result[:details] = details if details
158
+ result
159
+ end
160
+ end
161
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module ActiveadminMcp
4
2
  class RequestHandler
5
3
  PROTOCOL_VERSION = "2025-06-18"
@@ -40,45 +38,131 @@ module ActiveadminMcp
40
38
  end
41
39
 
42
40
  def tools_list
43
- {
44
- tools: [
45
- {
46
- name: "list_resources",
47
- description: "List the ActiveAdmin resources the authenticated user is authorized " \
48
- "to read, with their attributes",
49
- inputSchema: { type: "object", properties: {} },
41
+ { tools: built_in_tools + action_tools }
42
+ end
43
+
44
+ # Both gates run before the schema is built, never after: ActionSchema calls
45
+ # the application's `suggestions:` procs, which read the database. Filtering
46
+ # an already-assembled list would mean those procs had already run — and
47
+ # their values already been read — for a user authorized for none of it.
48
+ def action_tools
49
+ ActionCatalog.all.filter_map do |definition|
50
+ next unless authorized_to_run?(definition)
51
+ next unless authorized_to_list?(definition)
52
+
53
+ {
54
+ name: definition.tool_name,
55
+ description: definition.description,
56
+ inputSchema: ActionSchema.new(definition).to_h,
57
+ }
58
+ end
59
+ end
60
+
61
+ # The same authorization adapter check ActionRunner makes before dispatch,
62
+ # and the one list_resources makes for reads. There is no record at listing
63
+ # time, so the subject is the resource class.
64
+ def authorized_to_run?(definition)
65
+ Authorization.for(definition.config, @current_user)
66
+ .authorized?(definition.action_name, definition.config.resource_class)
67
+ rescue StandardError => e
68
+ warn("[activeadmin_mcp] hiding #{definition.tool_name}: authorization check raised #{e.class}: #{e.message}")
69
+ false
70
+ end
71
+
72
+ # Collection and batch actions whose permission proc takes no record can be
73
+ # resolved now, so the tool is simply hidden. A member action's proc needs a
74
+ # record, so its tool stays listed and refusal happens at call time.
75
+ #
76
+ # The proc is evaluated in controller context, exactly as ActionRunner
77
+ # evaluates it at call time, so `current_admin_user`, `can?` and the rest of
78
+ # the admin helpers are in scope. Evaluating it bare would make every such
79
+ # proc raise NameError and hide its tool from everybody.
80
+ def authorized_to_list?(definition)
81
+ permission = definition.permission
82
+ return true unless permission
83
+ return true unless permission.respond_to?(:arity) && permission.arity.zero?
84
+
85
+ controller = ControllerDispatcher.new(config: definition.config, current_user: @current_user)
86
+ .controller_with_mcp_user
87
+ !!::MethodOrProcHelper.render_in_context(controller, permission)
88
+ rescue StandardError => e
89
+ # One broken proc hides its own tool and nothing else, but it says so.
90
+ warn("[activeadmin_mcp] hiding #{definition.tool_name}: permission proc raised #{e.class}: #{e.message}")
91
+ false
92
+ end
93
+
94
+ def built_in_tools
95
+ [
96
+ {
97
+ name: "list_resources",
98
+ description: "List the ActiveAdmin resources the authenticated user is authorized " \
99
+ "to read, with their attributes",
100
+ inputSchema: { type: "object", properties: {} },
101
+ },
102
+ {
103
+ name: "query",
104
+ description: "Query an ActiveAdmin resource using Ransack syntax. Respects ActiveAdmin " \
105
+ "authorization: the resource must be readable by the authenticated user, " \
106
+ "and results are scoped to the records they may access.",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ resource: { type: "string", description: "Resource name (e.g., 'User', 'Post')" },
111
+ q: { type: "object", description: "Ransack query (e.g., {name_cont: 'john'})" },
112
+ limit: { type: "integer", description: "Max records (default: 25)" },
113
+ },
114
+ required: ["resource"],
50
115
  },
51
- {
52
- name: "query",
53
- description: "Query an ActiveAdmin resource using Ransack syntax. Respects ActiveAdmin " \
54
- "authorization: the resource must be readable by the authenticated user, " \
55
- "and results are scoped to the records they may access.",
56
- inputSchema: {
57
- type: "object",
58
- properties: {
59
- resource: { type: "string", description: "Resource name (e.g., 'User', 'Post')" },
60
- q: { type: "object", description: "Ransack query (e.g., {name_cont: 'john'})" },
61
- limit: { type: "integer", description: "Max records (default: 25)" },
116
+ },
117
+ {
118
+ name: "describe_form",
119
+ description: "Describe the fields of a resource's ActiveAdmin form — their input " \
120
+ "types, labels, hints, allowed values, column types and which are " \
121
+ "required — so a create or update call need not guess them.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ resource: { type: "string", description: "Resource name (e.g., 'User', 'Post')" },
126
+ action: {
127
+ type: "string",
128
+ enum: %w[new edit],
129
+ description: "Which form to describe: 'new' for creating, 'edit' for updating " \
130
+ "(default: 'new')",
62
131
  },
63
- required: ["resource"],
64
132
  },
133
+ required: ["resource"],
65
134
  },
66
- {
67
- name: "update",
68
- description: "Update an existing record. Only fields the resource's ActiveAdmin " \
69
- "form permits are written, and the update respects ActiveAdmin authorization.",
70
- inputSchema: {
71
- type: "object",
72
- properties: {
73
- resource: { type: "string", description: "Resource name (e.g., 'User', 'Post')" },
74
- id: { type: ["integer", "string"], description: "Primary key of the record to update" },
75
- attributes: { type: "object", description: "Attributes to update (e.g., {name: 'New name'})" },
76
- },
77
- required: %w[resource id attributes],
135
+ },
136
+ {
137
+ name: "create",
138
+ description: "Create a new record. The write runs through the resource's real " \
139
+ "ActiveAdmin create action, so only fields its permit_params accepts " \
140
+ "are written and its callbacks and authorization all apply.",
141
+ inputSchema: {
142
+ type: "object",
143
+ properties: {
144
+ resource: { type: "string", description: "Resource name (e.g., 'User', 'Post')" },
145
+ attributes: { type: "object", description: "Attributes for the new record (e.g., {name: 'Ada'})" },
78
146
  },
147
+ required: %w[resource attributes],
79
148
  },
80
- ],
81
- }
149
+ },
150
+ {
151
+ name: "update",
152
+ description: "Update an existing record. The write runs through the resource's real " \
153
+ "ActiveAdmin update action, so only fields its permit_params accepts " \
154
+ "are written and its callbacks and authorization all apply.",
155
+ inputSchema: {
156
+ type: "object",
157
+ properties: {
158
+ resource: { type: "string", description: "Resource name (e.g., 'User', 'Post')" },
159
+ id: { type: ["integer", "string"], description: "Primary key of the record to update" },
160
+ attributes: { type: "object", description: "Attributes to update (e.g., {name: 'New name'})" },
161
+ },
162
+ required: %w[resource id attributes],
163
+ },
164
+ },
165
+ ]
82
166
  end
83
167
 
84
168
  def call_tool(params)
@@ -88,13 +172,22 @@ module ActiveadminMcp
88
172
  result = case name
89
173
  when "list_resources" then tool_list_resources
90
174
  when "query" then tool_query(args)
175
+ when "describe_form" then tool_describe_form(args)
176
+ when "create" then tool_create(args)
91
177
  when "update" then tool_update(args)
92
- else { error: "Unknown tool: #{name}" }
178
+ else tool_action(name, args)
93
179
  end
94
180
 
95
181
  { content: [{ type: "text", text: JSON.pretty_generate(result) }] }
96
182
  end
97
183
 
184
+ def tool_action(name, args)
185
+ definition = ActionCatalog.find(name)
186
+ return { error: "Unknown tool: #{name}" } unless definition
187
+
188
+ ActionRunner.new(definition: definition, current_user: @current_user).call(args)
189
+ end
190
+
98
191
  def tool_list_resources
99
192
  entries = ResourceRegistry.resources.select { |entry| authorized_to_read?(entry) }
100
193
  { resources: entries.map { |entry| ResourceRegistry.resource_info(entry) } }
@@ -113,6 +206,24 @@ module ActiveadminMcp
113
206
  { resource: resource[:name], count: records.size, records: filter_sensitive(records.as_json) }
114
207
  end
115
208
 
209
+ def tool_describe_form(args)
210
+ resource = ResourceRegistry.find(args["resource"])
211
+ return { error: "Resource not found: #{args['resource']}" } unless resource
212
+
213
+ FormDescription.new(resource: resource, current_user: @current_user)
214
+ .call(action: args["action"] || "new")
215
+ end
216
+
217
+ def tool_create(args)
218
+ resource = ResourceRegistry.find(args["resource"])
219
+ return { error: "Resource not found: #{args['resource']}" } unless resource
220
+
221
+ attributes = args["attributes"] || {}
222
+ return { error: "attributes are required" } if attributes.empty?
223
+
224
+ RecordWriter.new(resource: resource, current_user: @current_user).create(attributes: attributes)
225
+ end
226
+
116
227
  def tool_update(args)
117
228
  resource = ResourceRegistry.find(args["resource"])
118
229
  return { error: "Resource not found: #{args['resource']}" } unless resource
@@ -121,8 +232,8 @@ module ActiveadminMcp
121
232
  attributes = args["attributes"] || {}
122
233
  return { error: "attributes are required" } if attributes.empty?
123
234
 
124
- RecordUpdater.new(resource: resource, current_user: @current_user)
125
- .call(id: args["id"], attributes: attributes)
235
+ RecordWriter.new(resource: resource, current_user: @current_user)
236
+ .update(id: args["id"], attributes: attributes)
126
237
  end
127
238
 
128
239
  def authorized_to_read?(resource)
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module ActiveadminMcp
4
2
  module ResourceRegistry
5
3
  class << self
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module ActiveadminMcp
4
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
5
3
  end
@@ -1,11 +1,17 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative "activeadmin_mcp/version"
4
2
  require_relative "activeadmin_mcp/configuration"
5
3
  require_relative "activeadmin_mcp/authorization"
4
+ require_relative "activeadmin_mcp/active_admin_ext"
5
+ require_relative "activeadmin_mcp/action_definition"
6
+ require_relative "activeadmin_mcp/action_schema"
7
+ require_relative "activeadmin_mcp/action_params"
8
+ require_relative "activeadmin_mcp/action_catalog"
9
+ require_relative "activeadmin_mcp/controller_dispatcher"
10
+ require_relative "activeadmin_mcp/action_runner"
6
11
  require_relative "activeadmin_mcp/resource_registry"
7
12
  require_relative "activeadmin_mcp/form_field_collector"
8
- require_relative "activeadmin_mcp/record_updater"
13
+ require_relative "activeadmin_mcp/form_description"
14
+ require_relative "activeadmin_mcp/record_writer"
9
15
  require_relative "activeadmin_mcp/request_handler"
10
16
  require_relative "activeadmin_mcp/engine"
11
17
 
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "rails/generators"
4
2
  require "rails/generators/active_record"
5
3
 
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  ActiveadminMcp.configure do |config|
4
2
  # Uncomment to enable API token authentication.
5
3
  # Requires running the auth migration first:
@@ -19,6 +17,11 @@ ActiveadminMcp.configure do |config|
19
17
  # Path where the MCP server is mounted.
20
18
  # config.mount_path = "/mcp"
21
19
 
20
+ # How the engine adds its route: :prepend (default), :append, or :none.
21
+ # Set it to :none when you want to mount the engine yourself, for instance
22
+ # inside a constraints block your admin routes already sit in.
23
+ # config.mount_strategy = :prepend
24
+
22
25
  # HTTP header used to read the Bearer token from.
23
26
  # Useful when a reverse proxy (e.g. AWS Verified Access) strips the
24
27
  # standard Authorization header.
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  ActiveAdmin.register_page "MCP API Tokens" do
4
2
  menu label: "MCP Tokens", parent: ActiveadminMcp.config.menu_parent, priority: 100
5
3
 
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  class CreateMcpApiTokens < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
2
  def change
5
3
  return if table_exists?(:mcp_api_tokens)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeadmin_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - harunkumars
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '6.1'
19
+ version: '7.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '6.1'
26
+ version: '7.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activeadmin
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '2.0'
33
+ version: '3.5'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '2.0'
40
+ version: '3.5'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -95,11 +95,19 @@ files:
95
95
  - app/models/activeadmin_mcp/api_token.rb
96
96
  - config/routes.rb
97
97
  - lib/activeadmin_mcp.rb
98
+ - lib/activeadmin_mcp/action_catalog.rb
99
+ - lib/activeadmin_mcp/action_definition.rb
100
+ - lib/activeadmin_mcp/action_params.rb
101
+ - lib/activeadmin_mcp/action_runner.rb
102
+ - lib/activeadmin_mcp/action_schema.rb
103
+ - lib/activeadmin_mcp/active_admin_ext.rb
98
104
  - lib/activeadmin_mcp/authorization.rb
99
105
  - lib/activeadmin_mcp/configuration.rb
106
+ - lib/activeadmin_mcp/controller_dispatcher.rb
100
107
  - lib/activeadmin_mcp/engine.rb
108
+ - lib/activeadmin_mcp/form_description.rb
101
109
  - lib/activeadmin_mcp/form_field_collector.rb
102
- - lib/activeadmin_mcp/record_updater.rb
110
+ - lib/activeadmin_mcp/record_writer.rb
103
111
  - lib/activeadmin_mcp/request_handler.rb
104
112
  - lib/activeadmin_mcp/resource_registry.rb
105
113
  - lib/activeadmin_mcp/version.rb
@@ -122,14 +130,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
130
  requirements:
123
131
  - - ">="
124
132
  - !ruby/object:Gem::Version
125
- version: 3.0.0
133
+ version: 4.0.0
126
134
  required_rubygems_version: !ruby/object:Gem::Requirement
127
135
  requirements:
128
136
  - - ">="
129
137
  - !ruby/object:Gem::Version
130
138
  version: '0'
131
139
  requirements: []
132
- rubygems_version: 3.6.9
140
+ rubygems_version: 4.0.20
133
141
  specification_version: 4
134
142
  summary: MCP server for Rails apps with ActiveAdmin
135
143
  test_files: []