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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: eff75b33a855b38a03f5b80a24ed92124bf4ffa6891a07cfac8683e2e9e7cb09
|
|
4
|
+
data.tar.gz: b8b8f6b7b6676f8fe7c6ef3ab7879135f7bfb500a675de35aa5e804636bd2049
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 31cfd450925701dc8ceffb2ad3135e154c8b28174189d56d1a50ac3ef369d5ece315d0d6a80e01b93189deecd6088e523d900cbb782ef0b4b713c413314a6e30
|
|
7
|
+
data.tar.gz: 750b76a58cc55a2ee206d762ab67749e7e827434e5bde50c07f1ddd590e7172aec6a803f6c249e30d39b346802fe4b5e888d1aad21617337ee6cfdb655b63792
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Servpac developers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
# ActiveScaffold MCP
|
|
2
|
+
|
|
3
|
+
`active_scaffold_mcp` generates ActionMCP tools from ActiveScaffold controller configuration.
|
|
4
|
+
|
|
5
|
+
The gem is under initial development. The first implementation will provide a configuration DSL, a generated-tool registry, and read-only list and show tools.
|
|
6
|
+
|
|
7
|
+
Association columns use ActiveScaffold's list settings in MCP results. An
|
|
8
|
+
unlimited collection (`associated_limit = nil`) is returned as an array. A
|
|
9
|
+
positive limit returns `{records: [...], count: N}` when the complete
|
|
10
|
+
association fits. A truncated association uses `first_records` instead of
|
|
11
|
+
`records`. The count is exact when `associated_number` is enabled; otherwise
|
|
12
|
+
MCP loads one extra record and reports a lower bound such as `count: ">3"`.
|
|
13
|
+
A zero limit is count-only (or `null` when `associated_number` is disabled).
|
|
14
|
+
Singular polymorphic associations also include their human model name as
|
|
15
|
+
`model`.
|
|
16
|
+
|
|
17
|
+
Configure the actions that may become MCP tools in the controller:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
active_scaffold :ticket do |config|
|
|
21
|
+
config.mcp.actions = {
|
|
22
|
+
list: {
|
|
23
|
+
columns: %i[id subject status customer_id],
|
|
24
|
+
filter_columns: {
|
|
25
|
+
id: %i[equal in between],
|
|
26
|
+
status: %i[equal in]
|
|
27
|
+
},
|
|
28
|
+
filters: %i[open overdue]
|
|
29
|
+
},
|
|
30
|
+
show: {
|
|
31
|
+
columns: %i[subject status description],
|
|
32
|
+
lookup_columns: %i[case_number]
|
|
33
|
+
},
|
|
34
|
+
update: {columns: %i[status description]}
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The same configuration can be written with the per-action DSL:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
active_scaffold :ticket do |config|
|
|
43
|
+
config.mcp.name = :support_tickets
|
|
44
|
+
config.mcp.label = "Support tickets"
|
|
45
|
+
config.mcp.expose :list,
|
|
46
|
+
description: "Find support tickets using queue filters and pagination.",
|
|
47
|
+
columns: %i[id subject status customer_id],
|
|
48
|
+
filter_columns: {status: %i[equal in]},
|
|
49
|
+
filters: %i[open overdue]
|
|
50
|
+
config.mcp.show.columns = %i[id subject status description]
|
|
51
|
+
config.mcp.show.lookup_columns = %i[case_number]
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`config.mcp.name` is the stable resource name shared by its generated tools.
|
|
56
|
+
When omitted, it uses `model.model_name.route_key`. Generators prefix it with
|
|
57
|
+
their action, producing names such as `list_support_tickets`; future action
|
|
58
|
+
generators can use the same value for names such as `show_support_ticket`.
|
|
59
|
+
|
|
60
|
+
`config.mcp.label` is the human-facing resource label shared by exposed
|
|
61
|
+
actions. Like ActiveScaffold's list label, it falls back to
|
|
62
|
+
`config.core.label(count: 2)`. In the example above the list title is
|
|
63
|
+
`List Support tickets`.
|
|
64
|
+
|
|
65
|
+
Both values can still be overridden for one action. An action-level `name` is
|
|
66
|
+
the complete tool identifier rather than a resource name:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
config.mcp.expose :list,
|
|
70
|
+
name: :search_support_tickets,
|
|
71
|
+
label: "Searchable support tickets"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
ActionMCP's `title` DSL stores `annotations.title`: a human-readable display
|
|
75
|
+
name that MCP clients may show. It is separate from `description`, which tells
|
|
76
|
+
the model what the tool does. The generated list description also states which
|
|
77
|
+
filter categories are available and that `limit` and `page` control pagination.
|
|
78
|
+
Set `description` explicitly when business-specific guidance would be more
|
|
79
|
+
useful. `name` remains the protocol identifier.
|
|
80
|
+
|
|
81
|
+
When the ActiveScaffold controller enables `config_list`, generated list tools
|
|
82
|
+
also expose `list_view` when configured or saved named views are enabled. The
|
|
83
|
+
agent-facing name is translated to ActiveScaffold's `config_list_view`
|
|
84
|
+
parameter. A view selects its columns and default sorting. Configured names are
|
|
85
|
+
listed in the description and used as the enum when they are the complete set.
|
|
86
|
+
When saved views are enabled, the property remains free-form and its description
|
|
87
|
+
names a generated `<list tool name>_views` tool. That companion tool returns
|
|
88
|
+
authorized configured views and current-user saved views with the exact `value`
|
|
89
|
+
to pass as `list_view`. Explicit MCP
|
|
90
|
+
`columns:` configuration supplies the default output when `list_view` is
|
|
91
|
+
omitted; an explicitly selected view supplies the output columns for that call.
|
|
92
|
+
|
|
93
|
+
Named-view security methods execute in the generated tool, like other list and
|
|
94
|
+
filter hooks. Define them in a shared module and add that module to the MCP list
|
|
95
|
+
action's `modules`; the gem does not instantiate the source Rails controller.
|
|
96
|
+
|
|
97
|
+
Use `expose` to introduce and configure a custom action that does not yet have
|
|
98
|
+
an MCP configuration entry:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
active_scaffold :customer do |config|
|
|
102
|
+
config.mcp.expose :sepa_mandate do |action|
|
|
103
|
+
action.columns = %i[account creditor_identifier]
|
|
104
|
+
action.description = "Create a SEPA mandate"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Set `config.mcp.actions = :all` to expose all enabled ActiveScaffold actions.
|
|
110
|
+
No actions are exposed by default. The configured tool generator is called when
|
|
111
|
+
the MCP action module is included in the controller.
|
|
112
|
+
|
|
113
|
+
Common actions can be exposed globally:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
ActiveScaffold.defaults do |config|
|
|
117
|
+
config.mcp.expose :list do |action|
|
|
118
|
+
action.filter_columns = {id: %i[equal in]}
|
|
119
|
+
end
|
|
120
|
+
config.mcp.expose :show
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Controller-specific custom actions should be exposed in that controller's
|
|
125
|
+
`active_scaffold` block.
|
|
126
|
+
|
|
127
|
+
Controller behavior that must also apply to an MCP tool should live in a shared
|
|
128
|
+
module. Include it in the controller and configure it for the action:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
module TicketListBehavior
|
|
132
|
+
def list_authorized?
|
|
133
|
+
current_user.has_right?("tickets", "index")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def beginning_of_chain
|
|
137
|
+
super.where(account_id: current_user.account_id)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class TicketsController < ApplicationController
|
|
142
|
+
active_scaffold :ticket do |config|
|
|
143
|
+
# Included in the controller after all ActiveScaffold action modules.
|
|
144
|
+
config.custom_modules << TicketListBehavior
|
|
145
|
+
|
|
146
|
+
# Included in the generated list tool after Actions::List.
|
|
147
|
+
config.mcp.expose :list, modules: TicketListBehavior
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Configured modules are included after the ActiveScaffold action module, so they
|
|
153
|
+
can override hooks such as `list_authorized?`, `beginning_of_chain`,
|
|
154
|
+
`conditions_for_collection`, and named-filter security methods. Keep HTTP-only
|
|
155
|
+
methods out of shared modules.
|
|
156
|
+
|
|
157
|
+
Record serialization is handled by `ActiveScaffold::MCP::Presenter`, separately
|
|
158
|
+
from the ActiveScaffold action modules included by the generated tool. A custom
|
|
159
|
+
presenter class may replace it for an action:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
class TicketMCPPresenter < ActiveScaffold::MCP::Presenter
|
|
163
|
+
# Override presentable_column?, record, or the protected presentation helpers.
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
config.mcp.expose :show, presenter: TicketMCPPresenter
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
A replacement which does not subclass the default presenter must accept the
|
|
170
|
+
tool in `initialize`, implement `presentable_column?`, and implement
|
|
171
|
+
`record(record, columns, association_counts)`. The presenter exposes its tool
|
|
172
|
+
and delegates `active_scaffold_config`, `params`, and `current_user` to it for
|
|
173
|
+
presentation which depends on the current invocation. Subclassing the default
|
|
174
|
+
presenter is recommended when only part of the standard serialization behavior
|
|
175
|
+
needs to change.
|
|
176
|
+
|
|
177
|
+
Use `presenter_modules` for action-specific presentation behavior without
|
|
178
|
+
creating a presenter subclass:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
module TicketMCPPresentation
|
|
182
|
+
def summary_column(record, _column)
|
|
183
|
+
record.summary.to_s.strip
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
config.mcp.expose :list, presenter_modules: TicketMCPPresentation
|
|
188
|
+
config.mcp.expose :show, presenter_modules: TicketMCPPresentation
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Each generated action gets its own anonymous presenter subclass when presenter
|
|
192
|
+
modules are configured, so the modules do not modify the base presenter or
|
|
193
|
+
other MCP tools. `modules` extends the tool's query and authorization behavior;
|
|
194
|
+
`presenter_modules` extends only record presentation.
|
|
195
|
+
|
|
196
|
+
Default filter-column sets can be configured globally:
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
ActiveScaffold.defaults do |config|
|
|
200
|
+
config.mcp.filter_column_sets = {
|
|
201
|
+
foreign_keys: :in,
|
|
202
|
+
timestamps: :range
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`filter_column_sets` requires a hash so every set has an explicit operator; the
|
|
208
|
+
gem does not assume whether a foreign key should use equality or membership, or
|
|
209
|
+
whether a timestamp should use equality or a range:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
config.mcp.expose :list,
|
|
213
|
+
filters: :all,
|
|
214
|
+
filter_column_sets: {foreign_keys: :in, timestamps: :range}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The generated base tool inherits from `ApplicationMCPTool` when that constant is
|
|
218
|
+
available, falling back to `ActionMCP::Tool`. It can be configured explicitly
|
|
219
|
+
before any generated tools are built:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
ActiveScaffold.defaults do |config|
|
|
223
|
+
config.mcp.base_tool = "ApplicationMCPTool"
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
An application base tool may define the class method
|
|
228
|
+
`active_scaffold_tool_authorized?` to filter generated tools for the current
|
|
229
|
+
user and apply the same application-level authorization before execution:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
class ApplicationMCPTool < ActionMCP::Tool
|
|
233
|
+
def self.active_scaffold_tool_authorized?(controller_class:, action_name:, user:)
|
|
234
|
+
user.has_right?(controller_class.controller_path_for_rights, action_name)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The hook is optional and should be a cheap check independent of tool arguments.
|
|
240
|
+
`ActionMCP::Configuration#filtered_tools` applies the active profile first and
|
|
241
|
+
then calls it using `ActionMCP::Current.user`. Tools without a class-level
|
|
242
|
+
authorization predicate are left unchanged. ActionMCP stores that filtered list
|
|
243
|
+
in the session, so a client must reconnect after its permissions change.
|
|
244
|
+
|
|
245
|
+
Before execution the generated tool checks the hook again inside
|
|
246
|
+
ActiveScaffold's current-user context. Action-specific instance checks are then
|
|
247
|
+
added by each tool type; for example, list tools also call `list_authorized?`.
|
|
248
|
+
|
|
249
|
+
When any execution-level authorization check returns `false`, the generated
|
|
250
|
+
tool calls the private instance method `unauthorized_tool!` instead of running
|
|
251
|
+
`perform`. By default it raises `ActiveScaffold::ActionNotAllowed`. The
|
|
252
|
+
configured application base tool may implement the method to use an
|
|
253
|
+
application-specific exception or message:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
class ApplicationMCPTool < ActionMCP::Tool
|
|
257
|
+
private
|
|
258
|
+
|
|
259
|
+
def unauthorized_tool!
|
|
260
|
+
raise ActiveScaffold::ActionNotAllowed,
|
|
261
|
+
"You are not allowed to use #{self.class.tool_name}"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The application implementation replaces the default fallback and should not
|
|
267
|
+
call `super`. If it returns normally, the callback still stops before
|
|
268
|
+
`perform`; ActionMCP will report the operation as aborted unless the hook has
|
|
269
|
+
already reported an error. This hook applies only during execution. Discovery
|
|
270
|
+
authorization is handled by the class-level `tool_authorized?` predicate.
|
|
271
|
+
|
|
272
|
+
### Inspecting registered and authorized tools
|
|
273
|
+
|
|
274
|
+
`action_mcp:list_tools` reads the complete ActionMCP registry and therefore
|
|
275
|
+
lists generated tools without applying profiles or per-user authorization.
|
|
276
|
+
`action_mcp:show_profile[profile_name]` calls
|
|
277
|
+
`ActionMCP::Configuration#filtered_tools`. If
|
|
278
|
+
`active_scaffold_tool_authorized?` requires a user, the generated tools are not
|
|
279
|
+
shown because a Rake task does not establish `ActionMCP::Current.user`.
|
|
280
|
+
|
|
281
|
+
ActionMCP's `show_profile` task accepts only `profile_name`. The gem adds a
|
|
282
|
+
prerequisite that can resolve the current user from `MCP_USER=CLASS#ID` before
|
|
283
|
+
the original task runs. The class may be namespaced and must respond to `find`;
|
|
284
|
+
the gem does not assume a particular user class or persistence library:
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
READONLY=1 RAILS_ENV=production MCP_USER='User#123' \
|
|
288
|
+
bin/rails 'action_mcp:show_profile[primary]'
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
For a namespaced class, use a value such as
|
|
292
|
+
`MCP_USER='Accounts::User#123'`. Without `MCP_USER`, the task leaves
|
|
293
|
+
`ActionMCP::Current.user` unset and retains ActionMCP's normal behavior.
|
|
294
|
+
|
|
295
|
+
The full accepted arguments are in each tool's `inputSchema`. ActionMCP can
|
|
296
|
+
print all registered schemas as JSON:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
READONLY=1 RAILS_ENV=production bin/rails action_mcp:apps:schema |
|
|
300
|
+
jq '.tools[] | select(.name == "list_support_tickets")'
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Known ActiveScaffold actions validate their supported settings. Settings for an
|
|
304
|
+
unknown custom action are accepted unless the configured validator implements a
|
|
305
|
+
matching validation method:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
class ApplicationMCPSettingsValidator < ActiveScaffold::MCP::SettingsValidator
|
|
309
|
+
private
|
|
310
|
+
|
|
311
|
+
def valid_settings_for_publish?(setting, value)
|
|
312
|
+
setting == :notify && [true, false].include?(value)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
ActiveScaffold.defaults do |config|
|
|
317
|
+
config.mcp.settings_validator = ApplicationMCPSettingsValidator
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Tool generators
|
|
322
|
+
|
|
323
|
+
`config.mcp.tool_generator` defaults to an
|
|
324
|
+
`ActiveScaffold::MCP::GeneratorFactory`. Generators may be registered for exact
|
|
325
|
+
actions or selected from action metadata:
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
ActiveScaffold.defaults do |config|
|
|
329
|
+
factory = config.mcp.tool_generator
|
|
330
|
+
|
|
331
|
+
factory.register(ListToolGenerator, actions: :list)
|
|
332
|
+
factory.register(ShowToolGenerator, actions: %i[show details])
|
|
333
|
+
|
|
334
|
+
factory.register_default(
|
|
335
|
+
FormToolGenerator,
|
|
336
|
+
crud_type: :create,
|
|
337
|
+
columns: true
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
factory.register_custom(
|
|
341
|
+
DestructiveActionToolGenerator,
|
|
342
|
+
submit_method: :post,
|
|
343
|
+
columns: false,
|
|
344
|
+
destructive: true
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
An action-specific generator implements:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
def self.generate_tool_for(
|
|
353
|
+
controller_class:,
|
|
354
|
+
action_name:,
|
|
355
|
+
action_config:,
|
|
356
|
+
mcp_config:,
|
|
357
|
+
settings:
|
|
358
|
+
)
|
|
359
|
+
# Return a generated tool class, an array of tool classes, or nil.
|
|
360
|
+
end
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The factory uses `generate_tools_for`.
|
|
364
|
+
|
|
365
|
+
The built-in `ListToolGenerator` is registered explicitly for `:list`, and
|
|
366
|
+
`ShowToolGenerator` is registered for `:show`. The list generator
|
|
367
|
+
generates a read-only subclass of `ActiveScaffold::MCP::ListTool`. The base list
|
|
368
|
+
tool reuses ActiveScaffold's `do_list` and owns execution, pagination,
|
|
369
|
+
authorization, and result serialization. The generator builds the concrete
|
|
370
|
+
tool metadata, input schema, parameter mappings, and configured extensions.
|
|
371
|
+
Generated list tools support:
|
|
372
|
+
|
|
373
|
+
- Explicit `filter_columns`, with `equal`, `in`, `between`, `null` and their
|
|
374
|
+
negated variants.
|
|
375
|
+
- The `foreign_keys`, `dates`, and `timestamps` filter-column sets.
|
|
376
|
+
- Selected named list filters or `filters: :all`. Named filters keep their
|
|
377
|
+
ActiveScaffold parameter name (for example, `filter_status`) in the tool schema.
|
|
378
|
+
- A `search` argument whenever ActiveScaffold's search action is enabled. It
|
|
379
|
+
uses the configured `config.search.columns` and can be combined with the other
|
|
380
|
+
list filters. Set `config.mcp.list.search = false` to omit it for MCP.
|
|
381
|
+
- Configurable output columns plus bounded `limit` and `page` arguments.
|
|
382
|
+
|
|
383
|
+
When a selected foreign key belongs to a polymorphic association, its type
|
|
384
|
+
column is exposed with the same operators. For example, selecting `company_id`
|
|
385
|
+
also exposes `company_type` (or `company_id_in` and `company_type_in` when using
|
|
386
|
+
the `in` operator).
|
|
387
|
+
|
|
388
|
+
The model primary key is always included in list and show output, even when it
|
|
389
|
+
is omitted from the configured MCP output columns, subject to the same column
|
|
390
|
+
and record authorization as every other value. Output columns selected from the
|
|
391
|
+
normal list configuration already pass through
|
|
392
|
+
ActiveScaffold's class-level visibility checks. Explicit MCP output columns are
|
|
393
|
+
checked with the model's `authorized_for?`, and every output value is also
|
|
394
|
+
checked with the individual record's `authorized_for?` before serialization.
|
|
395
|
+
Association columns have a stable representation: a singular association is
|
|
396
|
+
`{"id": ..., "label": ...}` or `null`, and a collection association is an
|
|
397
|
+
array of those objects. Labels use the associated record's `to_label`.
|
|
398
|
+
|
|
399
|
+
### Virtual output columns
|
|
400
|
+
|
|
401
|
+
When MCP `columns` are not configured explicitly, list and show tools reuse the
|
|
402
|
+
visible columns from `config.list.columns` and `config.show.columns`,
|
|
403
|
+
respectively. Those ActiveScaffold action columns may include virtual columns
|
|
404
|
+
which exist only for the HTML view and are rendered by helpers such as
|
|
405
|
+
`status_column(record, column)`.
|
|
406
|
+
|
|
407
|
+
MCP does not call ActiveScaffold column helpers. They are view-layer methods
|
|
408
|
+
which may return HTML, links, images, or other browser-specific output. During
|
|
409
|
+
list and show serialization, a non-association column is included only when the
|
|
410
|
+
record responds to the column name and that method can be called without
|
|
411
|
+
arguments. Consequently, a helper-only virtual column is omitted from the MCP
|
|
412
|
+
record instead of being returned as `null`:
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
active_scaffold :device do |config|
|
|
416
|
+
config.list.columns = %i[id name status_badge]
|
|
417
|
+
config.show.columns = %i[id name status_badge]
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
module DevicesHelper
|
|
421
|
+
def status_badge_column(record, column)
|
|
422
|
+
content_tag(:span, record.status, class: "badge")
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
In this example, generated list and show results contain `id` and `name`, but
|
|
428
|
+
not `status_badge`. The same rule applies when the virtual column is selected
|
|
429
|
+
explicitly with `config.mcp.list.columns`, `config.mcp.show.columns`, or the
|
|
430
|
+
`columns:` option passed to `config.mcp.expose`.
|
|
431
|
+
|
|
432
|
+
A virtual column backed by a public model instance method which accepts no
|
|
433
|
+
arguments is included, using the method's raw return value rather than a column
|
|
434
|
+
helper. Methods with only optional arguments can also be called without
|
|
435
|
+
arguments and are included. Model-backed columns whose value is `nil` are still
|
|
436
|
+
included as JSON `null`; omission is reserved for columns which are neither
|
|
437
|
+
associations nor readable from the record without arguments.
|
|
438
|
+
|
|
439
|
+
If an MCP tool needs a value which requires arguments or currently exists only
|
|
440
|
+
as HTML presentation logic, add a presenter module to the MCP action and define
|
|
441
|
+
`<column>_column(record, column)`, following the same naming convention as an
|
|
442
|
+
ActiveScaffold column helper. The method should return presentation-independent
|
|
443
|
+
data rather than HTML. Select the column for MCP output as usual:
|
|
444
|
+
|
|
445
|
+
```ruby
|
|
446
|
+
config.mcp.expose :show,
|
|
447
|
+
columns: %i[id status_badge],
|
|
448
|
+
presenter_modules: DeviceMCPPresentation
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
The result includes the records and ActiveScaffold's pagination metadata. The
|
|
452
|
+
total reuses the count already calculated by ActiveScaffold rather than issuing
|
|
453
|
+
another count query:
|
|
454
|
+
|
|
455
|
+
```json
|
|
456
|
+
{
|
|
457
|
+
"records": [],
|
|
458
|
+
"page": 2,
|
|
459
|
+
"total_count": 57,
|
|
460
|
+
"total_pages": 3
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
The show generator creates a read-only `show_<singular-resource>` tool. By default it
|
|
465
|
+
requires `id`, whose JSON type follows the model primary key. Configure
|
|
466
|
+
`lookup_columns` to expose alternative identifiers such as `case_number`:
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
config.mcp.show.lookup_columns = %i[case_number]
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Callers must provide exactly one of `id` or the configured lookup columns. A
|
|
473
|
+
lookup must resolve to exactly one record; a missing or ambiguous value returns
|
|
474
|
+
an error. Lookup columns are explicit because application-level uniqueness is
|
|
475
|
+
often more meaningful than database-index introspection. The tool reuses
|
|
476
|
+
ActiveScaffold's filtered query, `do_show`, show authorization, configured show
|
|
477
|
+
columns, and the same record and association serialization as list. Its result
|
|
478
|
+
contains one record:
|
|
479
|
+
|
|
480
|
+
```json
|
|
481
|
+
{
|
|
482
|
+
"record": {
|
|
483
|
+
"id": 7,
|
|
484
|
+
"name": "Widget 7"
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
For example, an `in` comparator creates `<column>_in`, while `between` creates
|
|
490
|
+
`<column>_from` and `<column>_to`. These are translated back to the parameter
|
|
491
|
+
shape consumed by ActiveScaffold's `conditions_from_params`.
|
|
492
|
+
|
|
493
|
+
## Development
|
|
494
|
+
|
|
495
|
+
The Servpac application loads the gem from `vendor/active_scaffold_mcp`:
|
|
496
|
+
|
|
497
|
+
```ruby
|
|
498
|
+
gem "active_scaffold_mcp", path: "vendor/active_scaffold_mcp"
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Run its tests from the gem directory with:
|
|
502
|
+
|
|
503
|
+
```sh
|
|
504
|
+
bundle exec rake test
|
|
505
|
+
```
|