active_scaffold_mcp 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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +505 -0
- data/lib/active_scaffold/actions/mcp.rb +14 -0
- data/lib/active_scaffold/config/mcp.rb +297 -0
- data/lib/active_scaffold/mcp/action_config.rb +38 -0
- data/lib/active_scaffold/mcp/engine.rb +23 -0
- data/lib/active_scaffold/mcp/filtered_tools.rb +18 -0
- data/lib/active_scaffold/mcp/flash.rb +11 -0
- data/lib/active_scaffold/mcp/generator_factory.rb +157 -0
- data/lib/active_scaffold/mcp/list_tool.rb +114 -0
- data/lib/active_scaffold/mcp/list_tool_generator.rb +339 -0
- data/lib/active_scaffold/mcp/list_views_tool.rb +45 -0
- data/lib/active_scaffold/mcp/presenter.rb +109 -0
- data/lib/active_scaffold/mcp/profile_user.rb +27 -0
- data/lib/active_scaffold/mcp/settings_validator.rb +81 -0
- data/lib/active_scaffold/mcp/show_tool.rb +68 -0
- data/lib/active_scaffold/mcp/show_tool_generator.rb +69 -0
- data/lib/active_scaffold/mcp/tool.rb +254 -0
- data/lib/active_scaffold/mcp/tool_generator.rb +85 -0
- data/lib/active_scaffold/mcp/version.rb +7 -0
- data/lib/active_scaffold/mcp.rb +61 -0
- data/lib/active_scaffold_mcp.rb +8 -0
- data/lib/tasks/active_scaffold_mcp_tasks.rake +11 -0
- metadata +104 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveScaffold
|
|
4
|
+
module MCP
|
|
5
|
+
class ShowToolGenerator < ToolGenerator
|
|
6
|
+
class << self
|
|
7
|
+
def generate_tool_for(controller_class:, action_name:, action_config:, mcp_config:, settings:)
|
|
8
|
+
core_config = mcp_config.core
|
|
9
|
+
primary_key = core_config.model.primary_key
|
|
10
|
+
raise ArgumentError, "cannot generate a show tool without a primary key" if primary_key.blank?
|
|
11
|
+
raise ArgumentError, "composite primary keys are not supported by the show tool" if primary_key.is_a?(Array)
|
|
12
|
+
lookup_columns = Array(settings[:lookup_columns]).map(&:to_sym).uniq - [primary_key.to_sym]
|
|
13
|
+
validate_lookup_columns!(core_config, lookup_columns)
|
|
14
|
+
|
|
15
|
+
Class.new(ActiveScaffold::MCP::ShowTool).tap do |tool_class|
|
|
16
|
+
configure_tool_class(tool_class, controller_class:, action_name:, core_config:, mcp_config:, settings:)
|
|
17
|
+
tool_class.mcp_lookup_column_names = lookup_columns.freeze
|
|
18
|
+
tool_class.mcp_primary_key_name = primary_key.to_sym
|
|
19
|
+
tool_class.include ActiveScaffold::Actions::List if core_config.actions.include?(:list)
|
|
20
|
+
tool_class.include ActiveScaffold::Actions::Show
|
|
21
|
+
tool_class.description(settings[:description] || description(tool_class.title, lookup_columns))
|
|
22
|
+
tool_class.read_only
|
|
23
|
+
tool_class.property :id,
|
|
24
|
+
**schema_options(core_config._columns_hash[primary_key.to_s]),
|
|
25
|
+
description: "Database ID of the #{singular_label(mcp_config)} to return",
|
|
26
|
+
required: lookup_columns.empty?
|
|
27
|
+
mappings = {id: {target: :id, operator: :equal}}
|
|
28
|
+
lookup_columns.each do |column_name|
|
|
29
|
+
tool_class.property column_name,
|
|
30
|
+
**schema_options(core_config._columns_hash[column_name.to_s]),
|
|
31
|
+
description: "#{column_name.to_s.humanize} of the #{singular_label(mcp_config)} to return"
|
|
32
|
+
mappings[column_name] = {target: column_name, operator: :equal}
|
|
33
|
+
end
|
|
34
|
+
tool_class.mcp_param_mappings = mappings.freeze
|
|
35
|
+
|
|
36
|
+
include_extensions(tool_class, settings)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def validate_lookup_columns!(core_config, lookup_columns)
|
|
43
|
+
unknown_columns = lookup_columns.reject { |name| core_config._columns_hash.key?(name.to_s) }
|
|
44
|
+
return if unknown_columns.empty?
|
|
45
|
+
|
|
46
|
+
raise ArgumentError, "unknown show lookup columns: #{unknown_columns.to_sentence}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tool_resource_name(mcp_config)
|
|
50
|
+
super.singularize
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def tool_resource_label(mcp_config, settings)
|
|
54
|
+
super.singularize
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def description(generated_title, lookup_columns)
|
|
58
|
+
identifiers = ["id", *lookup_columns.map(&:to_s)]
|
|
59
|
+
human_identifiers = identifiers.map { |name| name.humanize.downcase }
|
|
60
|
+
arguments = identifiers.map { |name| "`#{name}`" }
|
|
61
|
+
resource_label = generated_title.delete_prefix("Show ").singularize
|
|
62
|
+
"Retrieve one #{resource_label} by #{human_identifiers.to_sentence(two_words_connector: ' or ', last_word_connector: ' or ')}. " \
|
|
63
|
+
"Use this tool when the user supplies one of these identifiers. " \
|
|
64
|
+
"Pass exactly one of #{arguments.to_sentence(two_words_connector: ' or ', last_word_connector: ' or ')}."
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveScaffold
|
|
4
|
+
module MCP
|
|
5
|
+
class Tool < ActiveScaffold::Config::MCP.resolve_tool(:base)
|
|
6
|
+
abstract!
|
|
7
|
+
|
|
8
|
+
class_attribute :active_scaffold_config, instance_accessor: false
|
|
9
|
+
class_attribute :active_scaffold_controller_class, instance_accessor: false
|
|
10
|
+
class_attribute :mcp_action_name, instance_accessor: false
|
|
11
|
+
class_attribute :mcp_output_column_names, instance_accessor: false
|
|
12
|
+
class_attribute :mcp_param_mappings, instance_accessor: false, default: {}
|
|
13
|
+
class_attribute :mcp_presenter_class, instance_accessor: false, default: ActiveScaffold::MCP::Presenter
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def parameter_filter
|
|
17
|
+
filters = Rails.application&.config&.filter_parameters || []
|
|
18
|
+
return @parameter_filter if @parameter_filter_filters == filters
|
|
19
|
+
|
|
20
|
+
@parameter_filter_filters = filters.dup.freeze
|
|
21
|
+
@parameter_filter = ActiveSupport::ParameterFilter.new(filters)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def tool_authorized?(user = ActionMCP::Current.user)
|
|
25
|
+
return true unless respond_to?(:active_scaffold_tool_authorized?)
|
|
26
|
+
|
|
27
|
+
active_scaffold_tool_authorized?(
|
|
28
|
+
controller_class: active_scaffold_controller_class,
|
|
29
|
+
action_name: mcp_action_name,
|
|
30
|
+
user: user
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ActiveScaffold action modules declare Rails controller callbacks and
|
|
35
|
+
# helpers when included. MCP tools invoke their operation methods
|
|
36
|
+
# directly, so these declarations intentionally do nothing.
|
|
37
|
+
def before_action(...); end
|
|
38
|
+
def prepend_before_action(...); end
|
|
39
|
+
def after_action(...); end
|
|
40
|
+
def around_action(...); end
|
|
41
|
+
def helper_method(...); end
|
|
42
|
+
def rescue_from(...); end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
include ActiveScaffold::Finder
|
|
46
|
+
include ActiveScaffold::Constraints
|
|
47
|
+
include ActiveScaffold::Actions::Core
|
|
48
|
+
|
|
49
|
+
around_perform :with_active_scaffold_context
|
|
50
|
+
|
|
51
|
+
def initialize(attributes = {})
|
|
52
|
+
@provided_mcp_arguments = attributes.respond_to?(:keys) ? attributes.keys.map(&:to_s) : []
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def active_scaffold_config
|
|
57
|
+
@active_scaffold_config ||= begin
|
|
58
|
+
config = self.class.active_scaffold_config
|
|
59
|
+
config.new_user_settings({}, params)
|
|
60
|
+
config.user
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def params
|
|
65
|
+
@params ||= ::ActionController::Parameters.new(build_active_scaffold_params)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def flash
|
|
69
|
+
@flash ||= ActiveScaffold::MCP::Flash.new
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def action_name
|
|
73
|
+
self.class.mcp_action_name.to_s
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def active_scaffold_session_storage(_id = nil)
|
|
77
|
+
@active_scaffold_session_storage ||= ActiveSupport::HashWithIndifferentAccess.new
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def mcp_presenter
|
|
81
|
+
@mcp_presenter ||= self.class.mcp_presenter_class.new(self)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def with_active_scaffold_context
|
|
87
|
+
previous_user_proc = ActiveScaffold::Registry.current_user_proc
|
|
88
|
+
ActiveScaffold::Registry.current_user_proc = -> { current_user }
|
|
89
|
+
if tool_authorized?
|
|
90
|
+
yield
|
|
91
|
+
else
|
|
92
|
+
unauthorized_tool!
|
|
93
|
+
end
|
|
94
|
+
rescue ActiveScaffold::ActionNotAllowed
|
|
95
|
+
report_error(action_not_allowed_message)
|
|
96
|
+
rescue StandardError => exception
|
|
97
|
+
report_active_scaffold_exception(exception)
|
|
98
|
+
raise
|
|
99
|
+
ensure
|
|
100
|
+
ActiveScaffold::Registry.current_user_proc = previous_user_proc
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def report_active_scaffold_exception(exception)
|
|
104
|
+
arguments = mcp_exception_arguments
|
|
105
|
+
Rails.logger.error(
|
|
106
|
+
"[ActiveScaffold::MCP] #{self.class.tool_name} failed with arguments #{arguments.inspect}: " \
|
|
107
|
+
"#{exception.full_message(highlight: false, order: :top)}"
|
|
108
|
+
)
|
|
109
|
+
notify_active_scaffold_exception(exception) if respond_to?(:notify_active_scaffold_exception, true)
|
|
110
|
+
rescue StandardError => notification_exception
|
|
111
|
+
Rails.logger.error(
|
|
112
|
+
"[ActiveScaffold::MCP] exception notification failed for #{self.class.tool_name}: " \
|
|
113
|
+
"#{notification_exception.full_message(highlight: false, order: :top)}"
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def action_not_allowed_message
|
|
118
|
+
"The requested #{action_name.humanize.downcase} action is not allowed for the current user or request context."
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def mcp_exception_arguments
|
|
122
|
+
arguments = if defined?(@_wire_arguments)
|
|
123
|
+
@_wire_arguments
|
|
124
|
+
else
|
|
125
|
+
attributes.compact.merge(@_provided_additional_attributes)
|
|
126
|
+
end
|
|
127
|
+
ActiveScaffold::MCP::Tool.parameter_filter.filter(arguments.deep_dup)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Let the configured base tool customize execution authorization failures.
|
|
131
|
+
# Its implementation replaces this fallback and is responsible for
|
|
132
|
+
# reporting the failure, normally by raising an application-specific error.
|
|
133
|
+
def unauthorized_tool!
|
|
134
|
+
return super if defined?(super)
|
|
135
|
+
|
|
136
|
+
raise ActiveScaffold::ActionNotAllowed
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def tool_authorized?
|
|
140
|
+
self.class.tool_authorized?(current_user)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def build_active_scaffold_params
|
|
144
|
+
filter_values = {}
|
|
145
|
+
ranges = Hash.new { |hash, key| hash[key] = {} }
|
|
146
|
+
|
|
147
|
+
self.class.mcp_param_mappings.each do |property_name, mapping|
|
|
148
|
+
next unless @provided_mcp_arguments.include?(property_name.to_s)
|
|
149
|
+
|
|
150
|
+
value = public_send(property_name)
|
|
151
|
+
target = mapping.fetch(:target).to_s
|
|
152
|
+
case mapping.fetch(:operator)
|
|
153
|
+
when :equal, :in
|
|
154
|
+
filter_values[target] = value
|
|
155
|
+
when :not_equal, :not_in
|
|
156
|
+
filter_values["#{target}!"] = value
|
|
157
|
+
when :between_from, :between_to, :not_between_from, :not_between_to
|
|
158
|
+
ranges[[target, mapping[:operator].to_s.start_with?("not_")]][mapping[:operator].to_s.end_with?("from") ? :from : :to] = value
|
|
159
|
+
when :null
|
|
160
|
+
filter_values[target] = "" if value
|
|
161
|
+
when :not_null
|
|
162
|
+
filter_values["#{target}!"] = "" if value
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
ranges.each do |(target, negated), endpoints|
|
|
167
|
+
filter_values["#{target}#{'!' if negated}"] = "#{endpoints[:from]}..#{endpoints[:to]}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
filter_values.merge(format: "json")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def mcp_output_columns
|
|
174
|
+
configured_columns = mcp_configured_output_column_names
|
|
175
|
+
columns = if configured_columns
|
|
176
|
+
configured_columns.filter_map do |column_name|
|
|
177
|
+
next unless active_scaffold_config.model.authorized_for?(crud_type: :read, column: column_name)
|
|
178
|
+
|
|
179
|
+
active_scaffold_config.columns[column_name]
|
|
180
|
+
end
|
|
181
|
+
else
|
|
182
|
+
mcp_default_output_columns
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
(mcp_primary_key_columns + columns).uniq(&:name).select do |column|
|
|
186
|
+
mcp_presenter.presentable_column?(column)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def mcp_configured_output_column_names
|
|
191
|
+
self.class.mcp_output_column_names
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def mcp_record_hash(record, columns)
|
|
195
|
+
columns = columns.select do |column|
|
|
196
|
+
record.authorized_for?(crud_type: :read, column: column.name)
|
|
197
|
+
end
|
|
198
|
+
mcp_presenter.record(record, columns, association_counts_for(record, columns))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def mcp_primary_key_columns
|
|
202
|
+
Array(active_scaffold_config.model.primary_key).filter_map do |column_name|
|
|
203
|
+
next unless active_scaffold_config.model.authorized_for?(crud_type: :read, column: column_name)
|
|
204
|
+
|
|
205
|
+
active_scaffold_config.columns[column_name]
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def association_counts_for(record, columns)
|
|
210
|
+
columns.each_with_object({}) do |column, result|
|
|
211
|
+
cached_counts = @counts&.dig(column.name)
|
|
212
|
+
next unless cached_counts
|
|
213
|
+
|
|
214
|
+
key = column.association.primary_key if count_on_association_class?(column)
|
|
215
|
+
result[column.name] = cached_counts[record.public_send(key || :id)] || 0
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def mcp_associated_record_hash(record, polymorphic: false)
|
|
220
|
+
result = {id: record.id, label: record.to_label}
|
|
221
|
+
result[:model] = record.class.model_name.human if polymorphic
|
|
222
|
+
result
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def mcp_default_output_columns
|
|
226
|
+
raise NotImplementedError
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def mcp_argument_provided?(name)
|
|
230
|
+
@provided_mcp_arguments.include?(name.to_s)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The controller implementation branches on request methods and renders
|
|
234
|
+
# an HTTP response. MCP operations already represent the submitted branch.
|
|
235
|
+
def process_action_link_action(_render_action = :action_update, crud_type_or_security_options = nil)
|
|
236
|
+
@action_link = active_scaffold_config.action_links[action_name]
|
|
237
|
+
if params[:id]
|
|
238
|
+
security_options = crud_type_or_security_options || {crud_type: :update, action: action_name.to_sym}
|
|
239
|
+
get_row(security_options)
|
|
240
|
+
if @record
|
|
241
|
+
yield @record
|
|
242
|
+
else
|
|
243
|
+
self.successful = false
|
|
244
|
+
flash[:error] = "Record is not available for #{action_name}"
|
|
245
|
+
end
|
|
246
|
+
else
|
|
247
|
+
raise ActiveScaffold::ActionNotAllowed unless action_link_authorized?(@action_link)
|
|
248
|
+
|
|
249
|
+
yield
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveScaffold
|
|
4
|
+
module MCP
|
|
5
|
+
class ToolGenerator
|
|
6
|
+
class << self
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def configure_tool_class(tool_class, controller_class:, action_name:, core_config:, mcp_config:, settings:)
|
|
10
|
+
tool_class.active_scaffold_config = core_config
|
|
11
|
+
# Finder is included by the base MCP tool before a controller's
|
|
12
|
+
# ActiveScaffold config is known. Re-extending its class methods now
|
|
13
|
+
# lets Finder select the configured persistence adapter (and provide
|
|
14
|
+
# adapter-specific helpers such as like_pattern).
|
|
15
|
+
tool_class.extend ActiveScaffold::Finder::ClassMethods
|
|
16
|
+
tool_class.active_scaffold_controller_class = controller_class
|
|
17
|
+
tool_class.mcp_action_name = action_name
|
|
18
|
+
tool_class.mcp_output_column_names = settings[:columns]&.map(&:to_sym)
|
|
19
|
+
tool_class.mcp_presenter_class = build_presenter_class(settings)
|
|
20
|
+
tool_class.tool_name(settings[:name] || default_tool_name(action_name, mcp_config))
|
|
21
|
+
tool_class.title("#{action_name.to_s.humanize} #{tool_resource_label(mcp_config, settings)}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_tool_name(action_name, mcp_config)
|
|
25
|
+
"#{action_name}_#{tool_resource_name(mcp_config)}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def tool_resource_name(mcp_config)
|
|
29
|
+
mcp_config.name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def tool_resource_label(mcp_config, settings)
|
|
33
|
+
settings[:label] || mcp_config.label
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def include_extensions(tool_class, settings)
|
|
37
|
+
Array(settings[:modules]).each { |extension| tool_class.include extension }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_presenter_class(settings)
|
|
41
|
+
presenter_class = settings[:presenter] || ActiveScaffold::MCP::Presenter
|
|
42
|
+
presenter_modules = Array(settings[:presenter_modules])
|
|
43
|
+
return presenter_class if presenter_modules.empty?
|
|
44
|
+
|
|
45
|
+
Class.new(presenter_class).tap do |configured_class|
|
|
46
|
+
presenter_modules.each { |extension| configured_class.include extension }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def singular_label(mcp_config)
|
|
51
|
+
mcp_config.label.to_s.singularize
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def schema_options(column)
|
|
55
|
+
options = {type: json_type(column)}
|
|
56
|
+
format = json_format(column)
|
|
57
|
+
options[:format] = format if format
|
|
58
|
+
options
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def json_type(column)
|
|
62
|
+
case column&.type&.to_sym
|
|
63
|
+
when :integer, :bigint
|
|
64
|
+
"integer"
|
|
65
|
+
when :float, :decimal
|
|
66
|
+
"number"
|
|
67
|
+
when :boolean
|
|
68
|
+
"boolean"
|
|
69
|
+
else
|
|
70
|
+
"string"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def json_format(column)
|
|
75
|
+
case column&.type&.to_sym
|
|
76
|
+
when :date
|
|
77
|
+
"date"
|
|
78
|
+
when :datetime, :timestamp, :timestamptz
|
|
79
|
+
"date-time"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_scaffold/mcp/filtered_tools"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module ActiveScaffold
|
|
7
|
+
module Actions
|
|
8
|
+
autoload :MCP, "active_scaffold/actions/mcp"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Config
|
|
12
|
+
autoload :MCP, "active_scaffold/config/mcp"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module MCP
|
|
16
|
+
autoload :ActionConfig, "active_scaffold/mcp/action_config"
|
|
17
|
+
autoload :Flash, "active_scaffold/mcp/flash"
|
|
18
|
+
autoload :GeneratorFactory, "active_scaffold/mcp/generator_factory"
|
|
19
|
+
autoload :ListToolGenerator, "active_scaffold/mcp/list_tool_generator"
|
|
20
|
+
autoload :Presenter, "active_scaffold/mcp/presenter"
|
|
21
|
+
autoload :ProfileUser, "active_scaffold/mcp/profile_user"
|
|
22
|
+
autoload :SettingsValidator, "active_scaffold/mcp/settings_validator"
|
|
23
|
+
autoload :Tool, "active_scaffold/mcp/tool"
|
|
24
|
+
autoload :ToolGenerator, "active_scaffold/mcp/tool_generator"
|
|
25
|
+
autoload :ListTool, "active_scaffold/mcp/list_tool"
|
|
26
|
+
autoload :ShowTool, "active_scaffold/mcp/show_tool"
|
|
27
|
+
autoload :ListViewsTool, "active_scaffold/mcp/list_views_tool"
|
|
28
|
+
autoload :ShowToolGenerator, "active_scaffold/mcp/show_tool_generator"
|
|
29
|
+
autoload :VERSION, "active_scaffold/mcp/version"
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def generated_controller_names
|
|
33
|
+
@generated_controller_names ||= Set.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def generated_tool_classes
|
|
37
|
+
@generated_tool_classes ||= Set.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def generate_tools_for(controller_class, action_config)
|
|
41
|
+
generator = ActiveScaffold::Config::MCP.tool_generator
|
|
42
|
+
return [] unless generator
|
|
43
|
+
|
|
44
|
+
generated_controller_names << controller_class.name if controller_class.name.present?
|
|
45
|
+
generator.generate_tools_for(controller_class, action_config).tap do |tools|
|
|
46
|
+
generated_tool_classes.merge(Array(tools).compact)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reload_generated_tools!
|
|
51
|
+
controller_classes = generated_controller_names.filter_map(&:safe_constantize)
|
|
52
|
+
stale_tool_classes = generated_tool_classes.dup
|
|
53
|
+
ActionMCP::ToolsRegistry.items.delete_if { |_name, tool_class| stale_tool_classes.include?(tool_class) }
|
|
54
|
+
generated_tool_classes.clear
|
|
55
|
+
controller_classes.each do |controller_class|
|
|
56
|
+
generate_tools_for(controller_class, controller_class.active_scaffold_config.mcp)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :action_mcp do
|
|
4
|
+
task load_profile_user: :environment do
|
|
5
|
+
next if ENV["MCP_USER"].blank?
|
|
6
|
+
|
|
7
|
+
ActionMCP::Current.user = ActiveScaffold::MCP::ProfileUser.resolve(ENV.fetch("MCP_USER"))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
task show_profile: :load_profile_user
|
|
11
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: active_scaffold_mcp
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sergio Cambra
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: actionmcp
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.200'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.200'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: active_scaffold
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '4.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '4.3'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rails
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '8.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '8.1'
|
|
54
|
+
description: An ActionMCP integration that generates MCP tools from ActiveScaffold
|
|
55
|
+
configuration.
|
|
56
|
+
executables: []
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- LICENSE.txt
|
|
61
|
+
- README.md
|
|
62
|
+
- lib/active_scaffold/actions/mcp.rb
|
|
63
|
+
- lib/active_scaffold/config/mcp.rb
|
|
64
|
+
- lib/active_scaffold/mcp.rb
|
|
65
|
+
- lib/active_scaffold/mcp/action_config.rb
|
|
66
|
+
- lib/active_scaffold/mcp/engine.rb
|
|
67
|
+
- lib/active_scaffold/mcp/filtered_tools.rb
|
|
68
|
+
- lib/active_scaffold/mcp/flash.rb
|
|
69
|
+
- lib/active_scaffold/mcp/generator_factory.rb
|
|
70
|
+
- lib/active_scaffold/mcp/list_tool.rb
|
|
71
|
+
- lib/active_scaffold/mcp/list_tool_generator.rb
|
|
72
|
+
- lib/active_scaffold/mcp/list_views_tool.rb
|
|
73
|
+
- lib/active_scaffold/mcp/presenter.rb
|
|
74
|
+
- lib/active_scaffold/mcp/profile_user.rb
|
|
75
|
+
- lib/active_scaffold/mcp/settings_validator.rb
|
|
76
|
+
- lib/active_scaffold/mcp/show_tool.rb
|
|
77
|
+
- lib/active_scaffold/mcp/show_tool_generator.rb
|
|
78
|
+
- lib/active_scaffold/mcp/tool.rb
|
|
79
|
+
- lib/active_scaffold/mcp/tool_generator.rb
|
|
80
|
+
- lib/active_scaffold/mcp/version.rb
|
|
81
|
+
- lib/active_scaffold_mcp.rb
|
|
82
|
+
- lib/tasks/active_scaffold_mcp_tasks.rake
|
|
83
|
+
homepage: https://github.com/activescaffold/active_scaffold_mcp
|
|
84
|
+
licenses:
|
|
85
|
+
- MIT
|
|
86
|
+
metadata: {}
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
require_paths:
|
|
89
|
+
- lib
|
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '3.4'
|
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0'
|
|
100
|
+
requirements: []
|
|
101
|
+
rubygems_version: 3.6.9
|
|
102
|
+
specification_version: 4
|
|
103
|
+
summary: Expose ActiveScaffold actions as MCP tools
|
|
104
|
+
test_files: []
|